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 org.apache.ibatis.annotations.AutomapConstructor;
019import org.apache.ibatis.annotations.Param;
020import org.apache.ibatis.binding.MapperMethod.ParamMap;
021import org.apache.ibatis.cache.CacheKey;
022import org.apache.ibatis.cursor.Cursor;
023import org.apache.ibatis.cursor.defaults.DefaultCursor;
024import org.apache.ibatis.executor.ErrorContext;
025import org.apache.ibatis.executor.Executor;
026import org.apache.ibatis.executor.ExecutorException;
027import org.apache.ibatis.executor.loader.ResultLoader;
028import org.apache.ibatis.executor.loader.ResultLoaderMap;
029import org.apache.ibatis.executor.parameter.ParameterHandler;
030import org.apache.ibatis.executor.result.DefaultResultContext;
031import org.apache.ibatis.executor.result.DefaultResultHandler;
032import org.apache.ibatis.executor.result.ResultMapException;
033import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
034import org.apache.ibatis.executor.resultset.ResultSetWrapper;
035import org.apache.ibatis.mapping.*;
036import org.apache.ibatis.reflection.MetaClass;
037import org.apache.ibatis.reflection.MetaObject;
038import org.apache.ibatis.reflection.ReflectorFactory;
039import org.apache.ibatis.reflection.factory.ObjectFactory;
040import org.apache.ibatis.session.*;
041import org.apache.ibatis.type.JdbcType;
042import org.apache.ibatis.type.TypeHandler;
043import org.apache.ibatis.type.TypeHandlerRegistry;
044import org.apache.ibatis.util.MapUtil;
045
046import java.lang.reflect.Constructor;
047import java.lang.reflect.Parameter;
048import java.sql.CallableStatement;
049import java.sql.ResultSet;
050import java.sql.SQLException;
051import java.sql.Statement;
052import java.text.MessageFormat;
053import java.util.*;
054
055/**
056 * 复制于 DefaultResultSetHandler,并开放若干方法,方便子类重写
057 *
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                if (resultMap.getId().startsWith("com.mybatisflex.core.row.RowMapper.selectFirstAndSecondColumnsAsMap")) {
294                    ResultSet resultSet = rsw.getResultSet();
295                    skipRows(resultSet, rowBounds);
296                    Map<Object, Object> row = new HashMap<>();
297                    while (!resultSet.isClosed() && resultSet.next()) {
298                        row.put(resultSet.getObject(1), resultSet.getObject(2));
299                    }
300                    List<Map<Object, Object>> mapArrayList = new ArrayList<>(1);
301                    mapArrayList.add(row);
302                    multipleResults.add(mapArrayList);
303                } else {
304                    DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
305                    handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
306                    multipleResults.add(defaultResultHandler.getResultList());
307                }
308            } else {
309                handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
310            }
311        } finally {
312            // issue #228 (close resultsets)
313            closeResultSet(rsw.getResultSet());
314        }
315    }
316
317    @SuppressWarnings("unchecked")
318    private List<Object> collapseSingleResultList(List<Object> multipleResults) {
319        return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;
320    }
321
322    //
323    // HANDLE ROWS FOR SIMPLE RESULTMAP
324    //
325
326    @Override
327    public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler,
328                                RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
329        if (resultMap.hasNestedResultMaps()) {
330            ensureNoRowBounds();
331            checkResultHandler();
332            handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
333        } else {
334            handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
335        }
336    }
337
338    private void ensureNoRowBounds() {
339        if (configuration.isSafeRowBoundsEnabled() && rowBounds != null
340            && (rowBounds.getLimit() < RowBounds.NO_ROW_LIMIT || rowBounds.getOffset() > RowBounds.NO_ROW_OFFSET)) {
341            throw new ExecutorException(
342                "Mapped Statements with nested result mappings cannot be safely constrained by RowBounds. "
343                    + "Use safeRowBoundsEnabled=false setting to bypass this check.");
344        }
345    }
346
347    @Override
348    protected void checkResultHandler() {
349        if (resultHandler != null && configuration.isSafeResultHandlerEnabled() && !mappedStatement.isResultOrdered()) {
350            throw new ExecutorException(
351                "Mapped Statements with nested result mappings cannot be safely used with a custom ResultHandler. "
352                    + "Use safeResultHandlerEnabled=false setting to bypass this check "
353                    + "or ensure your statement returns ordered data and set resultOrdered=true on it.");
354        }
355    }
356
357    private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,
358                                                   ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
359        DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
360        ResultSet resultSet = rsw.getResultSet();
361        skipRows(resultSet, rowBounds);
362        while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
363            ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
364            Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
365            storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
366        }
367    }
368
369    private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue,
370                             ResultMapping parentMapping, ResultSet rs) throws SQLException {
371        if (parentMapping != null) {
372            linkToParents(rs, parentMapping, rowValue);
373        } else {
374            callResultHandler(resultHandler, resultContext, rowValue);
375        }
376    }
377
378    @SuppressWarnings("unchecked" /* because ResultHandler<?> is always ResultHandler<Object> */)
379    private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext,
380                                   Object rowValue) {
381        resultContext.nextResultObject(rowValue);
382        ((ResultHandler<Object>) resultHandler).handleResult(resultContext);
383    }
384
385    private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) {
386        return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
387    }
388
389    private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
390        if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
391            if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
392                rs.absolute(rowBounds.getOffset());
393            }
394        } else {
395            for (int i = 0; i < rowBounds.getOffset(); i++) {
396                if (!rs.next()) {
397                    break;
398                }
399            }
400        }
401    }
402
403    //
404    // GET VALUE FROM ROW FOR SIMPLE RESULT MAP
405    //
406
407    private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
408        final ResultLoaderMap lazyLoader = new ResultLoaderMap();
409        Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
410        if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
411            final MetaObject metaObject = configuration.newMetaObject(rowValue);
412            boolean foundValues = this.useConstructorMappings;
413            if (shouldApplyAutomaticMappings(resultMap, false)) {
414                foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
415            }
416            foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
417            foundValues = lazyLoader.size() > 0 || foundValues;
418            rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
419        }
420        return rowValue;
421    }
422
423    //
424    // GET VALUE FROM ROW FOR NESTED RESULT MAP
425    //
426
427    private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix,
428                               Object partialObject) throws SQLException {
429        final String resultMapId = resultMap.getId();
430        Object rowValue = partialObject;
431        if (rowValue != null) {
432            final MetaObject metaObject = configuration.newMetaObject(rowValue);
433            putAncestor(rowValue, resultMapId);
434            applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
435            ancestorObjects.remove(resultMapId);
436        } else {
437            final ResultLoaderMap lazyLoader = new ResultLoaderMap();
438            rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
439            if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
440                final MetaObject metaObject = configuration.newMetaObject(rowValue);
441                boolean foundValues = this.useConstructorMappings;
442                if (shouldApplyAutomaticMappings(resultMap, true)) {
443                    foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
444                }
445                foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
446                putAncestor(rowValue, resultMapId);
447                foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true)
448                    || foundValues;
449                ancestorObjects.remove(resultMapId);
450                foundValues = lazyLoader.size() > 0 || foundValues;
451                rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
452            }
453            if (combinedKey != CacheKey.NULL_CACHE_KEY) {
454                nestedResultObjects.put(combinedKey, rowValue);
455            }
456        }
457        return rowValue;
458    }
459
460    private void putAncestor(Object resultObject, String resultMapId) {
461        ancestorObjects.put(resultMapId, resultObject);
462    }
463
464    private boolean shouldApplyAutomaticMappings(ResultMap resultMap, boolean isNested) {
465        if (resultMap.getAutoMapping() != null) {
466            return resultMap.getAutoMapping();
467        }
468        if (isNested) {
469            return AutoMappingBehavior.FULL == configuration.getAutoMappingBehavior();
470        } else {
471            return AutoMappingBehavior.NONE != configuration.getAutoMappingBehavior();
472        }
473    }
474
475    //
476    // PROPERTY MAPPINGS
477    //
478
479    private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
480                                          ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
481        final Collection<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
482        boolean foundValues = false;
483        final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
484        for (ResultMapping propertyMapping : propertyMappings) {
485            String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
486            if (propertyMapping.getNestedResultMapId() != null) {
487                // the user added a column attribute to a nested result map, ignore it
488                column = null;
489            }
490            if (propertyMapping.isCompositeResult()
491                || column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))
492                || propertyMapping.getResultSet() != null) {
493                Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader,
494                    columnPrefix);
495                // issue #541 make property optional
496                final String property = propertyMapping.getProperty();
497                if (property == null) {
498                    continue;
499                }
500                if (value == DEFERRED) {
501                    foundValues = true;
502                    continue;
503                }
504                if (value != null) {
505                    foundValues = true;
506                }
507                if (value != null
508                    || configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive()) {
509                    // gcode issue #377, call setter on nulls (value is not 'found')
510                    metaObject.setValue(property, value);
511                }
512            }
513        }
514        return foundValues;
515    }
516
517    private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,
518                                           ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
519        if (propertyMapping.getNestedQueryId() != null) {
520            return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
521        }
522        if (propertyMapping.getResultSet() != null) {
523            addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK?
524            return DEFERRED;
525        } else {
526            final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
527            final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
528            return typeHandler.getResult(rs, column);
529        }
530    }
531
532    private List<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap,
533                                                                    MetaObject metaObject, String columnPrefix) throws SQLException {
534        final String mapKey = resultMap.getId() + ":" + columnPrefix;
535        List<UnMappedColumnAutoMapping> autoMapping = autoMappingsCache.get(mapKey);
536        if (autoMapping == null) {
537            autoMapping = new ArrayList<>();
538            final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
539            // Remove the entry to release the memory
540            List<String> mappedInConstructorAutoMapping = constructorAutoMappingColumns.remove(mapKey);
541            if (mappedInConstructorAutoMapping != null) {
542                unmappedColumnNames.removeAll(mappedInConstructorAutoMapping);
543            }
544            for (String columnName : unmappedColumnNames) {
545                String propertyName = columnName;
546                if (columnPrefix != null && !columnPrefix.isEmpty()) {
547                    // When columnPrefix is specified,
548                    // ignore columns without the prefix.
549                    if (!columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
550                        continue;
551                    }
552                    propertyName = columnName.substring(columnPrefix.length());
553                }
554                final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
555                if (property != null && metaObject.hasSetter(property)) {
556                    if (resultMap.getMappedProperties().contains(property)) {
557                        continue;
558                    }
559                    final Class<?> propertyType = metaObject.getSetterType(property);
560                    if (typeHandlerRegistry.hasTypeHandler(propertyType, rsw.getJdbcType(columnName))) {
561                        final TypeHandler<?> typeHandler = rsw.getTypeHandler(propertyType, columnName);
562                        autoMapping
563                            .add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType.isPrimitive()));
564                    } else {
565                        configuration.getAutoMappingUnknownColumnBehavior().doAction(mappedStatement, columnName, property,
566                            propertyType);
567                    }
568                } else {
569                    configuration.getAutoMappingUnknownColumnBehavior().doAction(mappedStatement, columnName,
570                        property != null ? property : propertyName, null);
571                }
572            }
573            autoMappingsCache.put(mapKey, autoMapping);
574        }
575        return autoMapping;
576    }
577
578    private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
579                                           String columnPrefix) throws SQLException {
580        List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
581        boolean foundValues = false;
582        if (!autoMapping.isEmpty()) {
583            for (UnMappedColumnAutoMapping mapping : autoMapping) {
584                final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
585                if (value != null) {
586                    foundValues = true;
587                }
588                if (value != null || configuration.isCallSettersOnNulls() && !mapping.primitive) {
589                    // gcode issue #377, call setter on nulls (value is not 'found')
590                    metaObject.setValue(mapping.property, value);
591                }
592            }
593        }
594        return foundValues;
595    }
596
597    // MULTIPLE RESULT SETS
598
599    private void linkToParents(ResultSet rs, ResultMapping parentMapping, Object rowValue) throws SQLException {
600        CacheKey parentKey = createKeyForMultipleResults(rs, parentMapping, parentMapping.getColumn(),
601            parentMapping.getForeignColumn());
602        List<PendingRelation> parents = pendingRelations.get(parentKey);
603        if (parents != null) {
604            for (PendingRelation parent : parents) {
605                if (parent != null && rowValue != null) {
606                    linkObjects(parent.metaObject, parent.propertyMapping, rowValue);
607                }
608            }
609        }
610    }
611
612    private void addPendingChildRelation(ResultSet rs, MetaObject metaResultObject, ResultMapping parentMapping)
613        throws SQLException {
614        CacheKey cacheKey = createKeyForMultipleResults(rs, parentMapping, parentMapping.getColumn(),
615            parentMapping.getColumn());
616        PendingRelation deferLoad = new PendingRelation();
617        deferLoad.metaObject = metaResultObject;
618        deferLoad.propertyMapping = parentMapping;
619        List<PendingRelation> relations = MapUtil.computeIfAbsent(pendingRelations, cacheKey, k -> new ArrayList<>());
620        // issue #255
621        relations.add(deferLoad);
622        ResultMapping previous = nextResultMaps.get(parentMapping.getResultSet());
623        if (previous == null) {
624            nextResultMaps.put(parentMapping.getResultSet(), parentMapping);
625        } else if (!previous.equals(parentMapping)) {
626            throw new ExecutorException("Two different properties are mapped to the same resultSet");
627        }
628    }
629
630    private CacheKey createKeyForMultipleResults(ResultSet rs, ResultMapping resultMapping, String names, String columns)
631        throws SQLException {
632        CacheKey cacheKey = new CacheKey();
633        cacheKey.update(resultMapping);
634        if (columns != null && names != null) {
635            String[] columnsArray = columns.split(",");
636            String[] namesArray = names.split(",");
637            for (int i = 0; i < columnsArray.length; i++) {
638                Object value = rs.getString(columnsArray[i]);
639                if (value != null) {
640                    cacheKey.update(namesArray[i]);
641                    cacheKey.update(value);
642                }
643            }
644        }
645        return cacheKey;
646    }
647
648    //
649    // INSTANTIATION & CONSTRUCTOR MAPPING
650    //
651
652    private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader,
653                                      String columnPrefix) throws SQLException {
654        this.useConstructorMappings = false; // reset previous mapping result
655        final List<Class<?>> constructorArgTypes = new ArrayList<>();
656        final List<Object> constructorArgs = new ArrayList<>();
657        Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
658        if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
659            final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
660            for (ResultMapping propertyMapping : propertyMappings) {
661                // issue gcode #109 && issue #149
662                if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
663                    resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration,
664                        objectFactory, constructorArgTypes, constructorArgs);
665                    break;
666                }
667            }
668        }
669        this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
670        return resultObject;
671    }
672
673    private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes,
674                                      List<Object> constructorArgs, String columnPrefix) throws SQLException {
675        final Class<?> resultType = resultMap.getType();
676        final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
677        final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
678        if (hasTypeHandlerForResultObject(rsw, resultType)) {
679            return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
680        }
681        if (!constructorMappings.isEmpty()) {
682            return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs,
683                columnPrefix);
684        } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
685            return objectFactory.create(resultType);
686        } else if (shouldApplyAutomaticMappings(resultMap, false)) {
687            return createByConstructorSignature(rsw, resultMap, columnPrefix, resultType, constructorArgTypes,
688                constructorArgs);
689        }
690        throw new ExecutorException("Do not know how to create an instance of " + resultType);
691    }
692
693    Object createParameterizedResultObject(ResultSetWrapper rsw, Class<?> resultType,
694                                           List<ResultMapping> constructorMappings, List<Class<?>> constructorArgTypes, List<Object> constructorArgs,
695                                           String columnPrefix) {
696        boolean foundValues = false;
697        for (ResultMapping constructorMapping : constructorMappings) {
698            final Class<?> parameterType = constructorMapping.getJavaType();
699            final String column = constructorMapping.getColumn();
700            final Object value;
701            try {
702                if (constructorMapping.getNestedQueryId() != null) {
703                    value = getNestedQueryConstructorValue(rsw.getResultSet(), constructorMapping, columnPrefix);
704                } else if (constructorMapping.getNestedResultMapId() != null) {
705                    final ResultMap resultMap = configuration.getResultMap(constructorMapping.getNestedResultMapId());
706                    value = getRowValue(rsw, resultMap, getColumnPrefix(columnPrefix, constructorMapping));
707                } else {
708                    final TypeHandler<?> typeHandler = constructorMapping.getTypeHandler();
709                    value = typeHandler.getResult(rsw.getResultSet(), prependPrefix(column, columnPrefix));
710                }
711            } catch (ResultMapException | SQLException e) {
712                throw new ExecutorException("Could not process result for mapping: " + constructorMapping, e);
713            }
714            constructorArgTypes.add(parameterType);
715            constructorArgs.add(value);
716            foundValues = value != null || foundValues;
717        }
718        return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
719    }
720
721    private Object createByConstructorSignature(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix,
722                                                Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) throws SQLException {
723        return applyConstructorAutomapping(rsw, resultMap, columnPrefix, resultType, constructorArgTypes, constructorArgs,
724            findConstructorForAutomapping(resultType, rsw).orElseThrow(() -> new ExecutorException(
725                "No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames())));
726    }
727
728    private Optional<Constructor<?>> findConstructorForAutomapping(final Class<?> resultType, ResultSetWrapper rsw) {
729        Constructor<?>[] constructors = resultType.getDeclaredConstructors();
730        if (constructors.length == 1) {
731            return Optional.of(constructors[0]);
732        }
733        Optional<Constructor<?>> annotated = Arrays.stream(constructors)
734            .filter(x -> x.isAnnotationPresent(AutomapConstructor.class)).reduce((x, y) -> {
735                throw new ExecutorException("@AutomapConstructor should be used in only one constructor.");
736            });
737        if (annotated.isPresent()) {
738            return annotated;
739        }
740        if (configuration.isArgNameBasedConstructorAutoMapping()) {
741            // Finding-best-match type implementation is possible,
742            // but using @AutomapConstructor seems sufficient.
743            throw new ExecutorException(MessageFormat.format(
744                "'argNameBasedConstructorAutoMapping' is enabled and the class ''{0}'' has multiple constructors, so @AutomapConstructor must be added to one of the constructors.",
745                resultType.getName()));
746        } else {
747            return Arrays.stream(constructors).filter(x -> findUsableConstructorByArgTypes(x, rsw.getJdbcTypes())).findAny();
748        }
749    }
750
751    private boolean findUsableConstructorByArgTypes(final Constructor<?> constructor, final List<JdbcType> jdbcTypes) {
752        final Class<?>[] parameterTypes = constructor.getParameterTypes();
753        if (parameterTypes.length != jdbcTypes.size()) {
754            return false;
755        }
756        for (int i = 0; i < parameterTypes.length; i++) {
757            if (!typeHandlerRegistry.hasTypeHandler(parameterTypes[i], jdbcTypes.get(i))) {
758                return false;
759            }
760        }
761        return true;
762    }
763
764    private Object applyConstructorAutomapping(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix,
765                                               Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor)
766        throws SQLException {
767        boolean foundValues = false;
768        if (configuration.isArgNameBasedConstructorAutoMapping()) {
769            foundValues = applyArgNameBasedConstructorAutoMapping(rsw, resultMap, columnPrefix, constructorArgTypes,
770                constructorArgs, constructor, foundValues);
771        } else {
772            foundValues = applyColumnOrderBasedConstructorAutomapping(rsw, constructorArgTypes, constructorArgs, constructor,
773                foundValues);
774        }
775        return foundValues || configuration.isReturnInstanceForEmptyRow()
776            ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
777    }
778
779    private boolean applyColumnOrderBasedConstructorAutomapping(ResultSetWrapper rsw, List<Class<?>> constructorArgTypes,
780                                                                List<Object> constructorArgs, Constructor<?> constructor, boolean foundValues) throws SQLException {
781        for (int i = 0; i < constructor.getParameterTypes().length; i++) {
782            Class<?> parameterType = constructor.getParameterTypes()[i];
783
784            // https://github.com/mybatis-flex/mybatis-flex/pull/201
785            // Parameter parameter = constructor.getParameters()[i];
786            // String columnName = StringUtil.camelToUnderline(parameter.getName());
787
788            String columnName = rsw.getColumnNames().get(i);
789            TypeHandler<?> typeHandler = rsw.getTypeHandler(parameterType, columnName);
790            Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
791            constructorArgTypes.add(parameterType);
792            constructorArgs.add(value);
793            foundValues = value != null || foundValues;
794        }
795        return foundValues;
796    }
797
798    private boolean applyArgNameBasedConstructorAutoMapping(ResultSetWrapper rsw, ResultMap resultMap,
799                                                            String columnPrefix, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor,
800                                                            boolean foundValues) throws SQLException {
801        List<String> missingArgs = null;
802        Parameter[] params = constructor.getParameters();
803        for (Parameter param : params) {
804            boolean columnNotFound = true;
805            Param paramAnno = param.getAnnotation(Param.class);
806            String paramName = paramAnno == null ? param.getName() : paramAnno.value();
807            for (String columnName : rsw.getColumnNames()) {
808                if (columnMatchesParam(columnName, paramName, columnPrefix)) {
809                    Class<?> paramType = param.getType();
810                    TypeHandler<?> typeHandler = rsw.getTypeHandler(paramType, columnName);
811                    Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
812                    constructorArgTypes.add(paramType);
813                    constructorArgs.add(value);
814                    final String mapKey = resultMap.getId() + ":" + columnPrefix;
815                    if (!autoMappingsCache.containsKey(mapKey)) {
816                        MapUtil.computeIfAbsent(constructorAutoMappingColumns, mapKey, k -> new ArrayList<>()).add(columnName);
817                    }
818                    columnNotFound = false;
819                    foundValues = value != null || foundValues;
820                }
821            }
822            if (columnNotFound) {
823                if (missingArgs == null) {
824                    missingArgs = new ArrayList<>();
825                }
826                missingArgs.add(paramName);
827            }
828        }
829        if (foundValues && constructorArgs.size() < params.length) {
830            throw new ExecutorException(MessageFormat.format(
831                "Constructor auto-mapping of ''{1}'' failed " + "because ''{0}'' were not found in the result set; "
832                    + "Available columns are ''{2}'' and mapUnderscoreToCamelCase is ''{3}''.",
833                missingArgs, constructor, rsw.getColumnNames(), configuration.isMapUnderscoreToCamelCase()));
834        }
835        return foundValues;
836    }
837
838    private boolean columnMatchesParam(String columnName, String paramName, String columnPrefix) {
839        if (columnPrefix != null) {
840            if (!columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
841                return false;
842            }
843            columnName = columnName.substring(columnPrefix.length());
844        }
845        return paramName
846            .equalsIgnoreCase(configuration.isMapUnderscoreToCamelCase() ? columnName.replace("_", "") : columnName);
847    }
848
849    protected Object createPrimitiveResultObject(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix)
850        throws SQLException {
851        final Class<?> resultType = resultMap.getType();
852        final String columnName;
853        if (!resultMap.getResultMappings().isEmpty()) {
854            final List<ResultMapping> resultMappingList = resultMap.getResultMappings();
855            final ResultMapping mapping = resultMappingList.get(0);
856            columnName = prependPrefix(mapping.getColumn(), columnPrefix);
857        } else {
858            columnName = rsw.getColumnNames().get(0);
859        }
860        final TypeHandler<?> typeHandler = rsw.getTypeHandler(resultType, columnName);
861        return typeHandler.getResult(rsw.getResultSet(), columnName);
862    }
863
864    //
865    // NESTED QUERY
866    //
867
868    private Object getNestedQueryConstructorValue(ResultSet rs, ResultMapping constructorMapping, String columnPrefix)
869        throws SQLException {
870        final String nestedQueryId = constructorMapping.getNestedQueryId();
871        final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
872        final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
873        final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, constructorMapping,
874            nestedQueryParameterType, columnPrefix);
875        Object value = null;
876        if (nestedQueryParameterObject != null) {
877            final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
878            final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT,
879                nestedBoundSql);
880            final Class<?> targetType = constructorMapping.getJavaType();
881            final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery,
882                nestedQueryParameterObject, targetType, key, nestedBoundSql);
883            value = resultLoader.loadResult();
884        }
885        return value;
886    }
887
888    private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,
889                                              ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
890        final String nestedQueryId = propertyMapping.getNestedQueryId();
891        final String property = propertyMapping.getProperty();
892        final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
893        final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
894        final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping,
895            nestedQueryParameterType, columnPrefix);
896        Object value = null;
897        if (nestedQueryParameterObject != null) {
898            final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
899            final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT,
900                nestedBoundSql);
901            final Class<?> targetType = propertyMapping.getJavaType();
902            if (executor.isCached(nestedQuery, key)) {
903                executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
904                value = DEFERRED;
905            } else {
906                final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery,
907                    nestedQueryParameterObject, targetType, key, nestedBoundSql);
908                if (propertyMapping.isLazy()) {
909                    lazyLoader.addLoader(property, metaResultObject, resultLoader);
910                    value = DEFERRED;
911                } else {
912                    value = resultLoader.loadResult();
913                }
914            }
915        }
916        return value;
917    }
918
919    private Object prepareParameterForNestedQuery(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType,
920                                                  String columnPrefix) throws SQLException {
921        if (resultMapping.isCompositeResult()) {
922            return prepareCompositeKeyParameter(rs, resultMapping, parameterType, columnPrefix);
923        }
924        return prepareSimpleKeyParameter(rs, resultMapping, parameterType, columnPrefix);
925    }
926
927    private Object prepareSimpleKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType,
928                                             String columnPrefix) throws SQLException {
929//        final TypeHandler<?> typeHandler;
930//        if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
931//            typeHandler = typeHandlerRegistry.getTypeHandler(parameterType);
932//        } else {
933//            typeHandler = typeHandlerRegistry.getUnknownTypeHandler();
934//        }
935
936        TypeHandler<?> typeHandler = typeHandlerRegistry.getTypeHandler(parameterType);
937        if (typeHandler == null) {
938            typeHandler = typeHandlerRegistry.getUnknownTypeHandler();
939        }
940        return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
941    }
942
943    private Object prepareCompositeKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType,
944                                                String columnPrefix) throws SQLException {
945        final Object parameterObject = instantiateParameterObject(parameterType);
946        final MetaObject metaObject = configuration.newMetaObject(parameterObject);
947        boolean foundValues = false;
948        for (ResultMapping innerResultMapping : resultMapping.getComposites()) {
949            final Class<?> propType = metaObject.getSetterType(innerResultMapping.getProperty());
950            final TypeHandler<?> typeHandler = typeHandlerRegistry.getTypeHandler(propType);
951            final Object propValue = typeHandler.getResult(rs, prependPrefix(innerResultMapping.getColumn(), columnPrefix));
952            // issue #353 & #560 do not execute nested query if key is null
953            if (propValue != null) {
954                metaObject.setValue(innerResultMapping.getProperty(), propValue);
955                foundValues = true;
956            }
957        }
958        return foundValues ? parameterObject : null;
959    }
960
961    private Object instantiateParameterObject(Class<?> parameterType) {
962        if (parameterType == null) {
963            return new HashMap<>();
964        }
965        if (ParamMap.class.equals(parameterType)) {
966            return new HashMap<>(); // issue #649
967        } else {
968            return objectFactory.create(parameterType);
969        }
970    }
971
972    //
973    // DISCRIMINATOR
974    //
975
976    @Override
977    public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix)
978        throws SQLException {
979        Set<String> pastDiscriminators = new HashSet<>();
980        Discriminator discriminator = resultMap.getDiscriminator();
981        while (discriminator != null) {
982            final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);
983            final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));
984            if (!configuration.hasResultMap(discriminatedMapId)) {
985                break;
986            }
987            resultMap = configuration.getResultMap(discriminatedMapId);
988            Discriminator lastDiscriminator = discriminator;
989            discriminator = resultMap.getDiscriminator();
990            if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {
991                break;
992            }
993        }
994        return resultMap;
995    }
996
997    private Object getDiscriminatorValue(ResultSet rs, Discriminator discriminator, String columnPrefix)
998        throws SQLException {
999        final ResultMapping resultMapping = discriminator.getResultMapping();
1000        final TypeHandler<?> typeHandler = resultMapping.getTypeHandler();
1001        return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
1002    }
1003
1004
1005    protected String prependPrefix(String columnName, String prefix) {
1006        if (columnName == null || columnName.length() == 0 || prefix == null || prefix.length() == 0) {
1007            return columnName;
1008        }
1009        return prefix + columnName;
1010    }
1011
1012    //
1013    // HANDLE NESTED RESULT MAPS
1014    //
1015
1016    private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap,
1017                                                   ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
1018        final DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
1019        ResultSet resultSet = rsw.getResultSet();
1020        skipRows(resultSet, rowBounds);
1021        Object rowValue = previousRowValue;
1022        while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
1023            final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
1024            final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
1025            Object partialObject = nestedResultObjects.get(rowKey);
1026            // issue #577 && #542
1027            if (mappedStatement.isResultOrdered()) {
1028                if (partialObject == null && rowValue != null) {
1029                    nestedResultObjects.clear();
1030                    storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
1031                }
1032                rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
1033            } else {
1034                rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
1035                if (partialObject == null) {
1036                    storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
1037                }
1038            }
1039        }
1040        if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
1041            storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
1042            previousRowValue = null;
1043        } else if (rowValue != null) {
1044            previousRowValue = rowValue;
1045        }
1046    }
1047
1048    //
1049    // NESTED RESULT MAP (JOIN MAPPING)
1050    //
1051
1052    private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
1053                                              String parentPrefix, CacheKey parentRowKey, boolean newObject) {
1054        boolean foundValues = false;
1055        for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
1056            final String nestedResultMapId = resultMapping.getNestedResultMapId();
1057            if (nestedResultMapId != null && resultMapping.getResultSet() == null) {
1058                try {
1059                    final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
1060                    final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);
1061                    if (resultMapping.getColumnPrefix() == null) {
1062                        // try to fill circular reference only when columnPrefix
1063                        // is not specified for the nested result map (issue #215)
1064                        Object ancestorObject = ancestorObjects.get(nestedResultMapId);
1065                        if (ancestorObject != null) {
1066                            if (newObject) {
1067                                linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
1068                            }
1069                            continue;
1070                        }
1071                    }
1072                    final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
1073                    final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
1074                    Object rowValue = nestedResultObjects.get(combinedKey);
1075                    boolean knownValue = rowValue != null;
1076                    instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory
1077                    if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {
1078                        rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
1079                        if (rowValue != null && !knownValue) {
1080                            linkObjects(metaObject, resultMapping, rowValue);
1081                            foundValues = true;
1082                        }
1083                    }
1084                } catch (SQLException e) {
1085                    throw new ExecutorException(
1086                        "Error getting nested result map values for '" + resultMapping.getProperty() + "'.  Cause: " + e, e);
1087                }
1088            }
1089        }
1090        return foundValues;
1091    }
1092
1093    private String getColumnPrefix(String parentPrefix, ResultMapping resultMapping) {
1094        final StringBuilder columnPrefixBuilder = new StringBuilder();
1095        if (parentPrefix != null) {
1096            columnPrefixBuilder.append(parentPrefix);
1097        }
1098        if (resultMapping.getColumnPrefix() != null) {
1099            columnPrefixBuilder.append(resultMapping.getColumnPrefix());
1100        }
1101        return columnPrefixBuilder.length() == 0 ? null : columnPrefixBuilder.toString().toUpperCase(Locale.ENGLISH);
1102    }
1103
1104    private boolean anyNotNullColumnHasValue(ResultMapping resultMapping, String columnPrefix, ResultSetWrapper rsw)
1105        throws SQLException {
1106        Set<String> notNullColumns = resultMapping.getNotNullColumns();
1107        if (notNullColumns != null && !notNullColumns.isEmpty()) {
1108            ResultSet rs = rsw.getResultSet();
1109            for (String column : notNullColumns) {
1110                rs.getObject(prependPrefix(column, columnPrefix));
1111                if (!rs.wasNull()) {
1112                    return true;
1113                }
1114            }
1115            return false;
1116        }
1117        if (columnPrefix != null) {
1118            for (String columnName : rsw.getColumnNames()) {
1119                if (columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix.toUpperCase(Locale.ENGLISH))) {
1120                    return true;
1121                }
1122            }
1123            return false;
1124        }
1125        return true;
1126    }
1127
1128    private ResultMap getNestedResultMap(ResultSet rs, String nestedResultMapId, String columnPrefix)
1129        throws SQLException {
1130        ResultMap nestedResultMap = configuration.getResultMap(nestedResultMapId);
1131        return resolveDiscriminatedResultMap(rs, nestedResultMap, columnPrefix);
1132    }
1133
1134    //
1135    // UNIQUE RESULT KEY
1136    //
1137
1138    private CacheKey createRowKey(ResultMap resultMap, ResultSetWrapper rsw, String columnPrefix) throws SQLException {
1139        final CacheKey cacheKey = new CacheKey();
1140        cacheKey.update(resultMap.getId());
1141        List<ResultMapping> resultMappings = getResultMappingsForRowKey(resultMap);
1142        if (resultMappings.isEmpty()) {
1143            if (Map.class.isAssignableFrom(resultMap.getType())) {
1144                createRowKeyForMap(rsw, cacheKey);
1145            } else {
1146                createRowKeyForUnmappedProperties(resultMap, rsw, cacheKey, columnPrefix);
1147            }
1148        } else {
1149            createRowKeyForMappedProperties(resultMap, rsw, cacheKey, resultMappings, columnPrefix);
1150        }
1151        if (cacheKey.getUpdateCount() < 2) {
1152            return CacheKey.NULL_CACHE_KEY;
1153        }
1154        return cacheKey;
1155    }
1156
1157    private CacheKey combineKeys(CacheKey rowKey, CacheKey parentRowKey) {
1158        if (rowKey.getUpdateCount() > 1 && parentRowKey.getUpdateCount() > 1) {
1159            CacheKey combinedKey;
1160            try {
1161                combinedKey = rowKey.clone();
1162            } catch (CloneNotSupportedException e) {
1163                throw new ExecutorException("Error cloning cache key.  Cause: " + e, e);
1164            }
1165            combinedKey.update(parentRowKey);
1166            return combinedKey;
1167        }
1168        return CacheKey.NULL_CACHE_KEY;
1169    }
1170
1171    private List<ResultMapping> getResultMappingsForRowKey(ResultMap resultMap) {
1172        List<ResultMapping> resultMappings = resultMap.getIdResultMappings();
1173        if (resultMappings.isEmpty()) {
1174            resultMappings = resultMap.getPropertyResultMappings();
1175        }
1176        return resultMappings;
1177    }
1178
1179    private void createRowKeyForMappedProperties(ResultMap resultMap, ResultSetWrapper rsw, CacheKey cacheKey,
1180                                                 List<ResultMapping> resultMappings, String columnPrefix) throws SQLException {
1181        for (ResultMapping resultMapping : resultMappings) {
1182            if (resultMapping.isSimple()) {
1183                final String column = prependPrefix(resultMapping.getColumn(), columnPrefix);
1184                final TypeHandler<?> th = resultMapping.getTypeHandler();
1185                Collection<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
1186                // Issue #114
1187                if (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) {
1188                    final Object value = th.getResult(rsw.getResultSet(), column);
1189                    if (value != null || configuration.isReturnInstanceForEmptyRow()) {
1190                        cacheKey.update(column);
1191                        cacheKey.update(value);
1192                    }
1193                }
1194            }
1195        }
1196    }
1197
1198    private void createRowKeyForUnmappedProperties(ResultMap resultMap, ResultSetWrapper rsw, CacheKey cacheKey,
1199                                                   String columnPrefix) throws SQLException {
1200        final MetaClass metaType = MetaClass.forClass(resultMap.getType(), reflectorFactory);
1201        List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
1202        for (String column : unmappedColumnNames) {
1203            String property = column;
1204            if (columnPrefix != null && !columnPrefix.isEmpty()) {
1205                // When columnPrefix is specified, ignore columns without the prefix.
1206                if (!column.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
1207                    continue;
1208                }
1209                property = column.substring(columnPrefix.length());
1210            }
1211            if (metaType.findProperty(property, configuration.isMapUnderscoreToCamelCase()) != null) {
1212                String value = rsw.getResultSet().getString(column);
1213                if (value != null) {
1214                    cacheKey.update(column);
1215                    cacheKey.update(value);
1216                }
1217            }
1218        }
1219    }
1220
1221    private void createRowKeyForMap(ResultSetWrapper rsw, CacheKey cacheKey) throws SQLException {
1222        List<String> columnNames = rsw.getColumnNames();
1223        for (String columnName : columnNames) {
1224            final String value = rsw.getResultSet().getString(columnName);
1225            if (value != null) {
1226                cacheKey.update(columnName);
1227                cacheKey.update(value);
1228            }
1229        }
1230    }
1231
1232    private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {
1233        final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);
1234        if (collectionProperty != null) {
1235            final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);
1236            targetMetaObject.add(rowValue);
1237        } else {
1238            metaObject.setValue(resultMapping.getProperty(), rowValue);
1239        }
1240    }
1241
1242    private Object instantiateCollectionPropertyIfAppropriate(ResultMapping resultMapping, MetaObject metaObject) {
1243        final String propertyName = resultMapping.getProperty();
1244        Object propertyValue = metaObject.getValue(propertyName);
1245        if (propertyValue == null) {
1246            Class<?> type = resultMapping.getJavaType();
1247            if (type == null) {
1248                type = metaObject.getSetterType(propertyName);
1249            }
1250            try {
1251                if (objectFactory.isCollection(type)) {
1252                    propertyValue = objectFactory.create(type);
1253                    metaObject.setValue(propertyName, propertyValue);
1254                    return propertyValue;
1255                }
1256            } catch (Exception e) {
1257                throw new ExecutorException(
1258                    "Error instantiating collection property for result '" + resultMapping.getProperty() + "'.  Cause: " + e,
1259                    e);
1260            }
1261        } else if (objectFactory.isCollection(propertyValue.getClass())) {
1262            return propertyValue;
1263        }
1264        return null;
1265    }
1266
1267    private boolean hasTypeHandlerForResultObject(ResultSetWrapper rsw, Class<?> resultType) {
1268        if (rsw.getColumnNames().size() == 1) {
1269            return typeHandlerRegistry.hasTypeHandler(resultType, rsw.getJdbcType(rsw.getColumnNames().get(0)));
1270        }
1271        return typeHandlerRegistry.hasTypeHandler(resultType);
1272    }
1273
1274}