• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mybatis / mybatis-3 / 3143

26 Jan 2026 12:02PM UTC coverage: 87.419%. Remained the same
3143

push

github

web-flow
Merge pull request #3610 from jeffgbutler/standalone-resultmap

New @NamedResultMap Annotation

3862 of 4675 branches covered (82.61%)

22 of 24 new or added lines in 2 files covered. (91.67%)

1 existing line in 1 file now uncovered.

9985 of 11422 relevant lines covered (87.42%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.79
/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java
1
/*
2
 *    Copyright 2009-2026 the original author or authors.
3
 *
4
 *    Licensed under the Apache License, Version 2.0 (the "License");
5
 *    you may not use this file except in compliance with the License.
6
 *    You may obtain a copy of the License at
7
 *
8
 *       https://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 *    Unless required by applicable law or agreed to in writing, software
11
 *    distributed under the License is distributed on an "AS IS" BASIS,
12
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 *    See the License for the specific language governing permissions and
14
 *    limitations under the License.
15
 */
16
package org.apache.ibatis.builder.annotation;
17

18
import java.io.IOException;
19
import java.io.InputStream;
20
import java.lang.annotation.Annotation;
21
import java.lang.reflect.Array;
22
import java.lang.reflect.GenericArrayType;
23
import java.lang.reflect.Method;
24
import java.lang.reflect.Parameter;
25
import java.lang.reflect.ParameterizedType;
26
import java.lang.reflect.Type;
27
import java.util.ArrayList;
28
import java.util.Arrays;
29
import java.util.Collection;
30
import java.util.HashMap;
31
import java.util.List;
32
import java.util.Map;
33
import java.util.Optional;
34
import java.util.Properties;
35
import java.util.Set;
36
import java.util.stream.Collectors;
37
import java.util.stream.Stream;
38

39
import org.apache.ibatis.annotations.AnnotationConstants;
40
import org.apache.ibatis.annotations.Arg;
41
import org.apache.ibatis.annotations.CacheNamespace;
42
import org.apache.ibatis.annotations.CacheNamespaceRef;
43
import org.apache.ibatis.annotations.Case;
44
import org.apache.ibatis.annotations.Delete;
45
import org.apache.ibatis.annotations.DeleteProvider;
46
import org.apache.ibatis.annotations.Insert;
47
import org.apache.ibatis.annotations.InsertProvider;
48
import org.apache.ibatis.annotations.Lang;
49
import org.apache.ibatis.annotations.MapKey;
50
import org.apache.ibatis.annotations.NamedResultMap;
51
import org.apache.ibatis.annotations.NamedResultMaps;
52
import org.apache.ibatis.annotations.Options;
53
import org.apache.ibatis.annotations.Options.FlushCachePolicy;
54
import org.apache.ibatis.annotations.Param;
55
import org.apache.ibatis.annotations.Property;
56
import org.apache.ibatis.annotations.Result;
57
import org.apache.ibatis.annotations.ResultMap;
58
import org.apache.ibatis.annotations.ResultOrdered;
59
import org.apache.ibatis.annotations.ResultType;
60
import org.apache.ibatis.annotations.Results;
61
import org.apache.ibatis.annotations.Select;
62
import org.apache.ibatis.annotations.SelectKey;
63
import org.apache.ibatis.annotations.SelectProvider;
64
import org.apache.ibatis.annotations.TypeDiscriminator;
65
import org.apache.ibatis.annotations.Update;
66
import org.apache.ibatis.annotations.UpdateProvider;
67
import org.apache.ibatis.binding.MapperMethod.ParamMap;
68
import org.apache.ibatis.builder.BuilderException;
69
import org.apache.ibatis.builder.CacheRefResolver;
70
import org.apache.ibatis.builder.IncompleteElementException;
71
import org.apache.ibatis.builder.MapperBuilderAssistant;
72
import org.apache.ibatis.builder.ResultMappingConstructorResolver;
73
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
74
import org.apache.ibatis.cursor.Cursor;
75
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
76
import org.apache.ibatis.executor.keygen.KeyGenerator;
77
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
78
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
79
import org.apache.ibatis.io.Resources;
80
import org.apache.ibatis.mapping.Discriminator;
81
import org.apache.ibatis.mapping.FetchType;
82
import org.apache.ibatis.mapping.MappedStatement;
83
import org.apache.ibatis.mapping.ResultFlag;
84
import org.apache.ibatis.mapping.ResultMapping;
85
import org.apache.ibatis.mapping.ResultSetType;
86
import org.apache.ibatis.mapping.SqlCommandType;
87
import org.apache.ibatis.mapping.SqlSource;
88
import org.apache.ibatis.mapping.StatementType;
89
import org.apache.ibatis.parsing.PropertyParser;
90
import org.apache.ibatis.reflection.ParamNameResolver;
91
import org.apache.ibatis.reflection.TypeParameterResolver;
92
import org.apache.ibatis.scripting.LanguageDriver;
93
import org.apache.ibatis.session.Configuration;
94
import org.apache.ibatis.session.ResultHandler;
95
import org.apache.ibatis.session.RowBounds;
96
import org.apache.ibatis.type.JdbcType;
97
import org.apache.ibatis.type.TypeHandler;
98
import org.apache.ibatis.type.UnknownTypeHandler;
99

100
/**
101
 * @author Clinton Begin
102
 * @author Kazuki Shimizu
103
 */
104
public class MapperAnnotationBuilder {
105

106
  private static final Set<Class<? extends Annotation>> statementAnnotationTypes = Stream
1✔
107
      .of(Select.class, Update.class, Insert.class, Delete.class, SelectProvider.class, UpdateProvider.class,
1✔
108
          InsertProvider.class, DeleteProvider.class)
109
      .collect(Collectors.toSet());
1✔
110

111
  private final Configuration configuration;
112
  private final MapperBuilderAssistant assistant;
113
  private final Class<?> type;
114

115
  public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
1✔
116
    String resource = type.getName().replace('.', '/') + ".java (best guess)";
1✔
117
    this.assistant = new MapperBuilderAssistant(configuration, resource);
1✔
118
    this.configuration = configuration;
1✔
119
    this.type = type;
1✔
120
  }
1✔
121

122
  public void parse() {
123
    String resource = type.toString();
1✔
124
    if (!configuration.isResourceLoaded(resource)) {
1!
125
      loadXmlResource();
1✔
126
      configuration.addLoadedResource(resource);
1✔
127
      assistant.setCurrentNamespace(type.getName());
1✔
128
      parseCache();
1✔
129
      parseCacheRef();
1✔
130

131
      if (type.isAnnotationPresent(NamedResultMap.class)) {
1✔
NEW
132
        parseNamedResultMap(type.getAnnotation(NamedResultMap.class));
×
133
      }
134

135
      if (type.isAnnotationPresent(NamedResultMaps.class)) {
1✔
136
        parseNamedResultMaps();
1✔
137
      }
138

139
      for (Method method : type.getMethods()) {
1✔
140
        if (!canHaveStatement(method)) {
1✔
141
          continue;
1✔
142
        }
143
        if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
1✔
144
            && method.getAnnotation(ResultMap.class) == null) {
1✔
145
          parseResultMap(method);
1✔
146
        }
147
        try {
148
          parseStatement(method);
1✔
149
        } catch (IncompleteElementException e) {
1✔
150
          configuration.addIncompleteMethod(new MethodResolver(this, method));
1✔
151
        }
1✔
152
      }
153
    }
154
    configuration.parsePendingMethods(false);
1✔
155
  }
1✔
156

157
  private static boolean canHaveStatement(Method method) {
158
    // issue #237
159
    return !method.isBridge() && !method.isDefault();
1✔
160
  }
161

162
  private void loadXmlResource() {
163
    // Spring may not know the real resource name so we check a flag
164
    // to prevent loading again a resource twice
165
    // this flag is set at XMLMapperBuilder#bindMapperForNamespace
166
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
1✔
167
      String xmlResource = type.getName().replace('.', '/') + ".xml";
1✔
168
      // #1347
169
      InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
1✔
170
      if (inputStream == null) {
1✔
171
        // Search XML mapper that is not in the module but in the classpath.
172
        try {
173
          inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
×
174
        } catch (IOException e2) {
1✔
175
          // ignore, resource is not required
176
        }
×
177
      }
178
      if (inputStream != null) {
1✔
179
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource,
1✔
180
            configuration.getSqlFragments(), type);
1✔
181
        xmlParser.parse();
1✔
182
      }
183
    }
184
  }
1✔
185

186
  private void parseCache() {
187
    CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
1✔
188
    if (cacheDomain != null) {
1✔
189
      Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
1!
190
      Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
1!
191
      Properties props = convertToProperties(cacheDomain.properties());
1✔
192
      assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size,
1✔
193
          cacheDomain.readWrite(), cacheDomain.blocking(), props);
1✔
194
    }
195
  }
1✔
196

197
  private Properties convertToProperties(Property[] properties) {
198
    if (properties.length == 0) {
1✔
199
      return null;
1✔
200
    }
201
    Properties props = new Properties();
1✔
202
    for (Property property : properties) {
1✔
203
      props.setProperty(property.name(), PropertyParser.parse(property.value(), configuration.getVariables()));
1✔
204
    }
205
    return props;
1✔
206
  }
207

208
  private void parseCacheRef() {
209
    CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
1✔
210
    if (cacheDomainRef != null) {
1✔
211
      Class<?> refType = cacheDomainRef.value();
1✔
212
      String refName = cacheDomainRef.name();
1✔
213
      if (refType == void.class && refName.isEmpty()) {
1✔
214
        throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
1✔
215
      }
216
      if (refType != void.class && !refName.isEmpty()) {
1✔
217
        throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
1✔
218
      }
219
      String namespace = refType != void.class ? refType.getName() : refName;
1✔
220
      try {
221
        assistant.useCacheRef(namespace);
1✔
222
      } catch (IncompleteElementException e) {
1✔
223
        configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
1✔
224
      }
1✔
225
    }
226
  }
1✔
227

228
  private String parseResultMap(Method method) {
229
    Class<?> returnType = getReturnType(method, type);
1✔
230
    Arg[] args = method.getAnnotationsByType(Arg.class);
1✔
231
    Result[] results = method.getAnnotationsByType(Result.class);
1✔
232
    TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
1✔
233
    String resultMapId = generateResultMapName(method);
1✔
234
    applyResultMap(resultMapId, returnType, args, results, typeDiscriminator);
1✔
235
    return resultMapId;
1✔
236
  }
237

238
  private void parseNamedResultMaps() {
239
    NamedResultMaps namedResultMaps = type.getAnnotation(NamedResultMaps.class);
1✔
240
    for (NamedResultMap namedResultMap : namedResultMaps.value()) {
1✔
241
      parseNamedResultMap(namedResultMap);
1✔
242
    }
243
  }
1✔
244

245
  private void parseNamedResultMap(NamedResultMap namedResultMap) {
246
    validateNamedResultMap(namedResultMap);
1✔
247

248
    String resultMapId = type.getName() + '.' + namedResultMap.id();
1✔
249
    Class<?> returnType = namedResultMap.javaType();
1✔
250
    Arg[] args = namedResultMap.constructorArguments();
1✔
251
    Result[] results = namedResultMap.propertyMappings();
1✔
252
    TypeDiscriminator typeDiscriminator = namedResultMap.typeDiscriminator();
1✔
253
    if (AnnotationConstants.NULL_TYPE_DISCRIMINATOR.equals(typeDiscriminator.column())) {
1✔
254
      typeDiscriminator = null;
1✔
255
    }
256

257
    applyResultMap(resultMapId, returnType, args, results, typeDiscriminator);
1✔
258
  }
1✔
259

260
  private void validateNamedResultMap(NamedResultMap namedResultMap) {
261
    if (!AnnotationConstants.NULL_TYPE_DISCRIMINATOR.equals(namedResultMap.typeDiscriminator().column())) {
1✔
262
      return;
1✔
263
    }
264

265
    if (namedResultMap.constructorArguments().length == 0 && namedResultMap.propertyMappings().length == 0) {
1✔
266
      throw new BuilderException("If there is no type discriminator, then the NamedResultMap annotation "
1✔
267
          + "requires at least one constructor argument or property mapping");
268
    }
269
  }
1✔
270

271
  private String generateResultMapName(Method method) {
272
    Results results = method.getAnnotation(Results.class);
1✔
273
    if (results != null && !results.id().isEmpty()) {
1✔
274
      return type.getName() + "." + results.id();
1✔
275
    }
276
    StringBuilder suffix = new StringBuilder();
1✔
277
    for (Class<?> c : method.getParameterTypes()) {
1✔
278
      suffix.append("-");
1✔
279
      suffix.append(c.getSimpleName());
1✔
280
    }
281
    if (suffix.length() < 1) {
1✔
282
      suffix.append("-void");
1✔
283
    }
284
    return type.getName() + "." + method.getName() + suffix;
1✔
285
  }
286

287
  private void applyResultMap(String resultMapId, Class<?> returnType, Arg[] args, Result[] results,
288
      TypeDiscriminator discriminator) {
289
    List<ResultMapping> resultMappings = new ArrayList<>();
1✔
290
    applyConstructorArgs(args, returnType, resultMappings, resultMapId);
1✔
291
    applyResults(results, returnType, resultMappings);
1✔
292
    Discriminator disc = applyDiscriminator(resultMapId, returnType, discriminator);
1✔
293
    // TODO add AutoMappingBehaviour
294
    assistant.addResultMap(resultMapId, returnType, null, disc, resultMappings, null);
1✔
295
    createDiscriminatorResultMaps(resultMapId, returnType, discriminator);
1✔
296
  }
1✔
297

298
  private void createDiscriminatorResultMaps(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
299
    if (discriminator != null) {
1✔
300
      for (Case c : discriminator.cases()) {
1✔
301
        String caseResultMapId = resultMapId + "-" + c.value();
1✔
302
        List<ResultMapping> resultMappings = new ArrayList<>();
1✔
303
        // issue #136
304
        applyConstructorArgs(c.constructArgs(), resultType, resultMappings, resultMapId);
1✔
305
        applyResults(c.results(), resultType, resultMappings);
1✔
306
        // TODO add AutoMappingBehaviour
307
        assistant.addResultMap(caseResultMapId, c.type(), resultMapId, null, resultMappings, null);
1✔
308
      }
309
    }
310
  }
1✔
311

312
  private Discriminator applyDiscriminator(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
313
    if (discriminator != null) {
1✔
314
      String column = discriminator.column();
1✔
315
      Class<?> javaType = discriminator.javaType() == void.class ? String.class : discriminator.javaType();
1!
316
      JdbcType jdbcType = discriminator.jdbcType() == JdbcType.UNDEFINED ? null : discriminator.jdbcType();
1!
317
      @SuppressWarnings("unchecked")
318
      Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>) (discriminator
319
          .typeHandler() == UnknownTypeHandler.class ? null : discriminator.typeHandler());
1✔
320
      Case[] cases = discriminator.cases();
1✔
321
      Map<String, String> discriminatorMap = new HashMap<>();
1✔
322
      for (Case c : cases) {
1✔
323
        String value = c.value();
1✔
324
        String caseResultMapId = resultMapId + "-" + value;
1✔
325
        discriminatorMap.put(value, caseResultMapId);
1✔
326
      }
327
      return assistant.buildDiscriminator(resultType, column, javaType, jdbcType, typeHandler, discriminatorMap);
1✔
328
    }
329
    return null;
1✔
330
  }
331

332
  void parseStatement(Method method) {
333
    final Class<?> parameterTypeClass = getParameterType(method);
1✔
334
    final ParamNameResolver paramNameResolver = new ParamNameResolver(configuration, method, type);
1✔
335
    final LanguageDriver languageDriver = getLanguageDriver(method);
1✔
336

337
    getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> {
1✔
338
      final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass,
1✔
339
          paramNameResolver, languageDriver, method);
340
      final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType();
1✔
341
      final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options) x.getAnnotation())
1✔
342
          .orElse(null);
1✔
343
      final ResultOrdered resultOrderedAnnotation = getAnnotationWrapper(method, false, ResultOrdered.class)
1✔
344
          .map(x -> (ResultOrdered) x.getAnnotation()).orElse(null);
1✔
345
      final String mappedStatementId = type.getName() + "." + method.getName();
1✔
346

347
      final KeyGenerator keyGenerator;
348
      String keyProperty = null;
1✔
349
      String keyColumn = null;
1✔
350
      if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
1✔
351
        // first check for SelectKey annotation - that overrides everything else
352
        SelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class)
1✔
353
            .map(x -> (SelectKey) x.getAnnotation()).orElse(null);
1✔
354
        if (selectKey != null) {
1✔
355
          keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method),
1✔
356
              paramNameResolver, languageDriver);
357
          keyProperty = selectKey.keyProperty();
1✔
358
        } else if (options == null) {
1✔
359
          keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
1✔
360
        } else {
361
          keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
1✔
362
          keyProperty = options.keyProperty();
1✔
363
          keyColumn = options.keyColumn();
1✔
364
        }
365
      } else {
1✔
366
        keyGenerator = NoKeyGenerator.INSTANCE;
1✔
367
      }
368

369
      Integer fetchSize = null;
1✔
370
      Integer timeout = null;
1✔
371
      StatementType statementType = StatementType.PREPARED;
1✔
372
      ResultSetType resultSetType = configuration.getDefaultResultSetType();
1✔
373
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
1✔
374
      boolean flushCache = !isSelect;
1✔
375
      boolean useCache = isSelect;
1✔
376
      if (options != null) {
1✔
377
        if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
1✔
378
          flushCache = true;
1✔
379
        } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
1✔
380
          flushCache = false;
1✔
381
        }
382
        useCache = options.useCache();
1✔
383
        // issue #348
384
        fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null;
1✔
385
        timeout = options.timeout() > -1 ? options.timeout() : null;
1✔
386
        statementType = options.statementType();
1✔
387
        if (options.resultSetType() != ResultSetType.DEFAULT) {
1✔
388
          resultSetType = options.resultSetType();
1✔
389
        }
390
      }
391

392
      boolean isResultOrdered = false;
1✔
393
      if (resultOrderedAnnotation != null) {
1✔
394
        isResultOrdered = resultOrderedAnnotation.value();
1✔
395
      }
396

397
      String resultMapId = null;
1✔
398
      if (isSelect) {
1✔
399
        ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
1✔
400
        if (resultMapAnnotation != null) {
1✔
401
          resultMapId = String.join(",", resultMapAnnotation.value());
1✔
402
        } else {
403
          resultMapId = generateResultMapName(method);
1✔
404
        }
405
      }
406

407
      assistant.addMappedStatement(mappedStatementId, sqlSource, statementType, sqlCommandType, fetchSize, timeout,
1✔
408
          // ParameterMapID
409
          null, parameterTypeClass, resultMapId, getReturnType(method, type), resultSetType, flushCache, useCache,
1✔
410
          // TODO gcode issue #577
411
          isResultOrdered, keyGenerator, keyProperty, keyColumn, statementAnnotation.getDatabaseId(), languageDriver,
1✔
412
          // ResultSets
413
          options != null ? nullOrEmpty(options.resultSets()) : null, statementAnnotation.isDirtySelect(),
1✔
414
          paramNameResolver);
415
    });
1✔
416
  }
1✔
417

418
  private LanguageDriver getLanguageDriver(Method method) {
419
    Lang lang = method.getAnnotation(Lang.class);
1✔
420
    Class<? extends LanguageDriver> langClass = null;
1✔
421
    if (lang != null) {
1✔
422
      langClass = lang.value();
1✔
423
    }
424
    return configuration.getLanguageDriver(langClass);
1✔
425
  }
426

427
  private Class<?> getParameterType(Method method) {
428
    Class<?> parameterType = null;
1✔
429
    Parameter[] parameters = method.getParameters();
1✔
430
    for (Parameter param : parameters) {
1✔
431
      Class<?> paramType = param.getType();
1✔
432
      if (RowBounds.class.isAssignableFrom(paramType) || ResultHandler.class.isAssignableFrom(paramType)) {
1✔
433
        continue;
1✔
434
      }
435
      if (parameterType == null && param.getAnnotation(Param.class) == null) {
1✔
436
        parameterType = paramType;
1✔
437
      } else {
438
        return ParamMap.class;
1✔
439
      }
440
    }
441
    return parameterType;
1✔
442
  }
443

444
  private static Class<?> getReturnType(Method method, Class<?> type) {
445
    Class<?> returnType = method.getReturnType();
1✔
446
    Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, type);
1✔
447
    if (resolvedReturnType instanceof Class) {
1✔
448
      returnType = (Class<?>) resolvedReturnType;
1✔
449
      if (returnType.isArray()) {
1✔
450
        returnType = returnType.getComponentType();
1✔
451
      }
452
      // gcode issue #508
453
      if (void.class.equals(returnType)) {
1✔
454
        ResultType rt = method.getAnnotation(ResultType.class);
1✔
455
        if (rt != null) {
1✔
456
          returnType = rt.value();
1✔
457
        }
458
      }
1✔
459
    } else if (resolvedReturnType instanceof ParameterizedType) {
1!
460
      ParameterizedType parameterizedType = (ParameterizedType) resolvedReturnType;
1✔
461
      Class<?> rawType = (Class<?>) parameterizedType.getRawType();
1✔
462
      if (Collection.class.isAssignableFrom(rawType) || Cursor.class.isAssignableFrom(rawType)) {
1✔
463
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
1✔
464
        if (actualTypeArguments != null && actualTypeArguments.length == 1) {
1!
465
          Type returnTypeParameter = actualTypeArguments[0];
1✔
466
          if (returnTypeParameter instanceof Class<?>) {
1✔
467
            returnType = (Class<?>) returnTypeParameter;
1✔
468
          } else if (returnTypeParameter instanceof ParameterizedType) {
1!
469
            // (gcode issue #443) actual type can be a also a parameterized type
470
            returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
1✔
471
          } else if (returnTypeParameter instanceof GenericArrayType) {
×
472
            Class<?> componentType = (Class<?>) ((GenericArrayType) returnTypeParameter).getGenericComponentType();
×
473
            // (gcode issue #525) support List<byte[]>
474
            returnType = Array.newInstance(componentType, 0).getClass();
×
475
          }
476
        }
477
      } else if (method.isAnnotationPresent(MapKey.class) && Map.class.isAssignableFrom(rawType)) {
1!
478
        // (gcode issue 504) Do not look into Maps if there is not MapKey annotation
479
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
1✔
480
        if (actualTypeArguments != null && actualTypeArguments.length == 2) {
1!
481
          Type returnTypeParameter = actualTypeArguments[1];
1✔
482
          if (returnTypeParameter instanceof Class<?>) {
1✔
483
            returnType = (Class<?>) returnTypeParameter;
1✔
484
          } else if (returnTypeParameter instanceof ParameterizedType) {
1!
485
            // (gcode issue 443) actual type can be a also a parameterized type
486
            returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
1✔
487
          }
488
        }
489
      } else if (Optional.class.equals(rawType)) {
1✔
490
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
1✔
491
        Type returnTypeParameter = actualTypeArguments[0];
1✔
492
        if (returnTypeParameter instanceof Class<?>) {
1!
493
          returnType = (Class<?>) returnTypeParameter;
1✔
494
        }
495
      }
496
    }
497

498
    return returnType;
1✔
499
  }
500

501
  private void applyResults(Result[] results, Class<?> resultType, List<ResultMapping> resultMappings) {
502
    for (Result result : results) {
1✔
503
      List<ResultFlag> flags = new ArrayList<>();
1✔
504
      if (result.id()) {
1✔
505
        flags.add(ResultFlag.ID);
1✔
506
      }
507
      @SuppressWarnings("unchecked")
508
      Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>) (result
509
          .typeHandler() == UnknownTypeHandler.class ? null : result.typeHandler());
1✔
510
      boolean hasNestedResultMap = hasNestedResultMap(result);
1✔
511
      ResultMapping resultMapping = assistant.buildResultMapping(resultType, nullOrEmpty(result.property()),
1✔
512
          nullOrEmpty(result.column()), result.javaType() == void.class ? null : result.javaType(),
1✔
513
          result.jdbcType() == JdbcType.UNDEFINED ? null : result.jdbcType(),
1✔
514
          hasNestedSelect(result) ? nestedSelectId(result) : null,
1✔
515
          hasNestedResultMap ? nestedResultMapId(result) : null, null,
1✔
516
          hasNestedResultMap ? findColumnPrefix(result) : null, typeHandler, flags, null, null, isLazy(result));
1✔
517
      resultMappings.add(resultMapping);
1✔
518
    }
519
  }
1✔
520

521
  private String findColumnPrefix(Result result) {
522
    String columnPrefix = result.one().columnPrefix();
1✔
523
    if (columnPrefix.isEmpty()) {
1✔
524
      columnPrefix = result.many().columnPrefix();
1✔
525
    }
526
    return columnPrefix;
1✔
527
  }
528

529
  private String nestedResultMapId(Result result) {
530
    String resultMapId = result.one().resultMap();
1✔
531
    if (resultMapId.isEmpty()) {
1✔
532
      resultMapId = result.many().resultMap();
1✔
533
    }
534
    if (!resultMapId.contains(".")) {
1✔
535
      resultMapId = type.getName() + "." + resultMapId;
1✔
536
    }
537
    return resultMapId;
1✔
538
  }
539

540
  private boolean hasNestedResultMap(Result result) {
541
    if (!result.one().resultMap().isEmpty() && !result.many().resultMap().isEmpty()) {
1!
542
      throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
×
543
    }
544
    return !result.one().resultMap().isEmpty() || !result.many().resultMap().isEmpty();
1✔
545
  }
546

547
  private String nestedSelectId(Result result) {
548
    String nestedSelect = result.one().select();
1✔
549
    if (nestedSelect.isEmpty()) {
1✔
550
      nestedSelect = result.many().select();
1✔
551
    }
552
    if (!nestedSelect.contains(".")) {
1✔
553
      nestedSelect = type.getName() + "." + nestedSelect;
1✔
554
    }
555
    return nestedSelect;
1✔
556
  }
557

558
  private boolean isLazy(Result result) {
559
    boolean isLazy = configuration.isLazyLoadingEnabled();
1✔
560
    if (!result.one().select().isEmpty() && FetchType.DEFAULT != result.one().fetchType()) {
1✔
561
      isLazy = result.one().fetchType() == FetchType.LAZY;
1!
562
    } else if (!result.many().select().isEmpty() && FetchType.DEFAULT != result.many().fetchType()) {
1✔
563
      isLazy = result.many().fetchType() == FetchType.LAZY;
1!
564
    }
565
    return isLazy;
1✔
566
  }
567

568
  private boolean hasNestedSelect(Result result) {
569
    if (!result.one().select().isEmpty() && !result.many().select().isEmpty()) {
1✔
570
      throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
1✔
571
    }
572
    return !result.one().select().isEmpty() || !result.many().select().isEmpty();
1✔
573
  }
574

575
  private void applyConstructorArgs(Arg[] args, Class<?> resultType, List<ResultMapping> resultMappings,
576
      String resultMapId) {
577
    final List<ResultMapping> mappings = new ArrayList<>();
1✔
578
    for (Arg arg : args) {
1✔
579
      List<ResultFlag> flags = new ArrayList<>();
1✔
580
      flags.add(ResultFlag.CONSTRUCTOR);
1✔
581
      if (arg.id()) {
1✔
582
        flags.add(ResultFlag.ID);
1✔
583
      }
584
      @SuppressWarnings("unchecked")
585
      Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>) (arg
586
          .typeHandler() == UnknownTypeHandler.class ? null : arg.typeHandler());
1✔
587
      ResultMapping resultMapping = assistant.buildResultMapping(resultType, nullOrEmpty(arg.name()),
1✔
588
          nullOrEmpty(arg.column()), arg.javaType() == void.class ? null : arg.javaType(),
1✔
589
          arg.jdbcType() == JdbcType.UNDEFINED ? null : arg.jdbcType(), nullOrEmpty(arg.select()),
1✔
590
          nullOrEmpty(arg.resultMap()), null, nullOrEmpty(arg.columnPrefix()), typeHandler, flags, null, null, false);
1✔
591
      mappings.add(resultMapping);
1✔
592
    }
593

594
    final ResultMappingConstructorResolver resolver = new ResultMappingConstructorResolver(configuration, mappings,
1✔
595
        resultType, resultMapId);
596
    resultMappings.addAll(resolver.resolveWithConstructor());
1✔
597
  }
1✔
598

599
  private String nullOrEmpty(String value) {
600
    return value == null || value.trim().isEmpty() ? null : value;
1!
601
  }
602

603
  private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId,
604
      Class<?> parameterTypeClass, ParamNameResolver paramNameResolver, LanguageDriver languageDriver) {
605
    String id = baseStatementId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
1✔
606
    Class<?> resultTypeClass = selectKeyAnnotation.resultType();
1✔
607
    StatementType statementType = selectKeyAnnotation.statementType();
1✔
608
    String keyProperty = selectKeyAnnotation.keyProperty();
1✔
609
    String keyColumn = selectKeyAnnotation.keyColumn();
1✔
610
    boolean executeBefore = selectKeyAnnotation.before();
1✔
611

612
    // defaults
613
    boolean useCache = false;
1✔
614
    KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
1✔
615
    Integer fetchSize = null;
1✔
616
    Integer timeout = null;
1✔
617
    boolean flushCache = false;
1✔
618
    String parameterMap = null;
1✔
619
    String resultMap = null;
1✔
620
    ResultSetType resultSetTypeEnum = null;
1✔
621
    String databaseId = selectKeyAnnotation.databaseId().isEmpty() ? null : selectKeyAnnotation.databaseId();
1✔
622

623
    SqlSource sqlSource = buildSqlSourceFromStrings(selectKeyAnnotation.statement(), parameterTypeClass,
1✔
624
        paramNameResolver, languageDriver);
625
    SqlCommandType sqlCommandType = SqlCommandType.SELECT;
1✔
626

627
    assistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
1✔
628
        parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, false, keyGenerator,
629
        keyProperty, keyColumn, databaseId, languageDriver, null, false, paramNameResolver);
630

631
    id = assistant.applyCurrentNamespace(id, false);
1✔
632

633
    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
1✔
634
    SelectKeyGenerator answer = new SelectKeyGenerator(keyStatement, executeBefore);
1✔
635
    configuration.addKeyGenerator(id, answer);
1✔
636
    return answer;
1✔
637
  }
638

639
  private SqlSource buildSqlSource(Annotation annotation, Class<?> parameterType, ParamNameResolver paramNameResolver,
640
      LanguageDriver languageDriver, Method method) {
641
    if (annotation instanceof Select) {
1✔
642
      return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, paramNameResolver, languageDriver);
1✔
643
    } else if (annotation instanceof Update) {
1✔
644
      return buildSqlSourceFromStrings(((Update) annotation).value(), parameterType, paramNameResolver, languageDriver);
1✔
645
    } else if (annotation instanceof Insert) {
1✔
646
      return buildSqlSourceFromStrings(((Insert) annotation).value(), parameterType, paramNameResolver, languageDriver);
1✔
647
    } else if (annotation instanceof Delete) {
1✔
648
      return buildSqlSourceFromStrings(((Delete) annotation).value(), parameterType, paramNameResolver, languageDriver);
1✔
649
    } else if (annotation instanceof SelectKey) {
1!
650
      return buildSqlSourceFromStrings(((SelectKey) annotation).statement(), parameterType, paramNameResolver,
×
651
          languageDriver);
652
    }
653
    return new ProviderSqlSource(assistant.getConfiguration(), annotation, type, method);
1✔
654
  }
655

656
  private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass,
657
      ParamNameResolver paramNameResolver, LanguageDriver languageDriver) {
658
    return languageDriver.createSqlSource(configuration, String.join(" ", strings).trim(), parameterTypeClass,
1✔
659
        paramNameResolver);
660
  }
661

662
  @SafeVarargs
663
  private final Optional<AnnotationWrapper> getAnnotationWrapper(Method method, boolean errorIfNoMatch,
664
      Class<? extends Annotation>... targetTypes) {
665
    return getAnnotationWrapper(method, errorIfNoMatch, Arrays.asList(targetTypes));
1✔
666
  }
667

668
  private Optional<AnnotationWrapper> getAnnotationWrapper(Method method, boolean errorIfNoMatch,
669
      Collection<Class<? extends Annotation>> targetTypes) {
670
    String databaseId = configuration.getDatabaseId();
1✔
671
    Map<String, AnnotationWrapper> statementAnnotations = targetTypes.stream()
1✔
672
        .flatMap(x -> Arrays.stream(method.getAnnotationsByType(x))).map(AnnotationWrapper::new)
1✔
673
        .collect(Collectors.toMap(AnnotationWrapper::getDatabaseId, x -> x, (existing, duplicate) -> {
1✔
674
          throw new BuilderException(
1✔
675
              String.format("Detected conflicting annotations '%s' and '%s' on '%s'.", existing.getAnnotation(),
1✔
676
                  duplicate.getAnnotation(), method.getDeclaringClass().getName() + "." + method.getName()));
1✔
677
        }));
678
    AnnotationWrapper annotationWrapper = null;
1✔
679
    if (databaseId != null) {
1✔
680
      annotationWrapper = statementAnnotations.get(databaseId);
1✔
681
    }
682
    if (annotationWrapper == null) {
1✔
683
      annotationWrapper = statementAnnotations.get("");
1✔
684
    }
685
    if (errorIfNoMatch && annotationWrapper == null && !statementAnnotations.isEmpty()) {
1✔
686
      // Annotations exist, but there is no matching one for the specified databaseId
687
      throw new BuilderException(String.format(
1✔
688
          "Could not find a statement annotation that correspond a current database or default statement on method '%s.%s'. Current database id is [%s].",
689
          method.getDeclaringClass().getName(), method.getName(), databaseId));
1✔
690
    }
691
    return Optional.ofNullable(annotationWrapper);
1✔
692
  }
693

694
  public static Class<?> getMethodReturnType(String mapperFqn, String localStatementId) {
695
    if (mapperFqn == null || localStatementId == null) {
1!
696
      return null;
×
697
    }
698
    try {
699
      Class<?> mapperClass = Resources.classForName(mapperFqn);
1✔
700
      for (Method method : mapperClass.getMethods()) {
1✔
701
        if (method.getName().equals(localStatementId) && canHaveStatement(method)) {
1!
702
          return getReturnType(method, mapperClass);
1✔
703
        }
704
      }
705
    } catch (ClassNotFoundException e) {
1✔
706
      // No corresponding mapper interface which is OK
707
    }
1✔
708
    return null;
1✔
709
  }
710

711
  private static class AnnotationWrapper {
712
    private final Annotation annotation;
713
    private final String databaseId;
714
    private final SqlCommandType sqlCommandType;
715
    private boolean dirtySelect;
716

717
    AnnotationWrapper(Annotation annotation) {
1✔
718
      this.annotation = annotation;
1✔
719
      if (annotation instanceof Select) {
1✔
720
        databaseId = ((Select) annotation).databaseId();
1✔
721
        sqlCommandType = SqlCommandType.SELECT;
1✔
722
        dirtySelect = ((Select) annotation).affectData();
1✔
723
      } else if (annotation instanceof Update) {
1✔
724
        databaseId = ((Update) annotation).databaseId();
1✔
725
        sqlCommandType = SqlCommandType.UPDATE;
1✔
726
      } else if (annotation instanceof Insert) {
1✔
727
        databaseId = ((Insert) annotation).databaseId();
1✔
728
        sqlCommandType = SqlCommandType.INSERT;
1✔
729
      } else if (annotation instanceof Delete) {
1✔
730
        databaseId = ((Delete) annotation).databaseId();
1✔
731
        sqlCommandType = SqlCommandType.DELETE;
1✔
732
      } else if (annotation instanceof SelectProvider) {
1✔
733
        databaseId = ((SelectProvider) annotation).databaseId();
1✔
734
        sqlCommandType = SqlCommandType.SELECT;
1✔
735
        dirtySelect = ((SelectProvider) annotation).affectData();
1✔
736
      } else if (annotation instanceof UpdateProvider) {
1✔
737
        databaseId = ((UpdateProvider) annotation).databaseId();
1✔
738
        sqlCommandType = SqlCommandType.UPDATE;
1✔
739
      } else if (annotation instanceof InsertProvider) {
1✔
740
        databaseId = ((InsertProvider) annotation).databaseId();
1✔
741
        sqlCommandType = SqlCommandType.INSERT;
1✔
742
      } else if (annotation instanceof DeleteProvider) {
1✔
743
        databaseId = ((DeleteProvider) annotation).databaseId();
1✔
744
        sqlCommandType = SqlCommandType.DELETE;
1✔
745
      } else {
746
        sqlCommandType = SqlCommandType.UNKNOWN;
1✔
747
        if (annotation instanceof Options) {
1✔
748
          databaseId = ((Options) annotation).databaseId();
1✔
749
        } else if (annotation instanceof SelectKey) {
1✔
750
          databaseId = ((SelectKey) annotation).databaseId();
1✔
751
        } else {
752
          databaseId = "";
1✔
753
        }
754
      }
755
    }
1✔
756

757
    Annotation getAnnotation() {
758
      return annotation;
1✔
759
    }
760

761
    SqlCommandType getSqlCommandType() {
762
      return sqlCommandType;
1✔
763
    }
764

765
    String getDatabaseId() {
766
      return databaseId;
1✔
767
    }
768

769
    boolean isDirtySelect() {
770
      return dirtySelect;
1✔
771
    }
772
  }
773
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc