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

mybatis / mybatis-3 / 2686

01 Feb 2025 09:55PM UTC coverage: 87.093% (-0.1%) from 87.217%
2686

Pull #3379

github

web-flow
Merge c97c5c598 into 3d71c862a
Pull Request #3379: Resolve type handler based on `java.lang.reflect.Type` instead of `Class` and respect runtime JDBC type

3825 of 4663 branches covered (82.03%)

515 of 579 new or added lines in 36 files covered. (88.95%)

28 existing lines in 6 files now uncovered.

9912 of 11381 relevant lines covered (87.09%)

0.87 hits per line

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

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

96
/**
97
 * @author Clinton Begin
98
 * @author Kazuki Shimizu
99
 */
100
public class MapperAnnotationBuilder {
101

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

107
  private final Configuration configuration;
108
  private final MapperBuilderAssistant assistant;
109
  private final Class<?> type;
110

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

118
  public void parse() {
119
    String resource = type.toString();
1✔
120
    if (!configuration.isResourceLoaded(resource)) {
1!
121
      loadXmlResource();
1✔
122
      configuration.addLoadedResource(resource);
1✔
123
      assistant.setCurrentNamespace(type.getName());
1✔
124
      parseCache();
1✔
125
      parseCacheRef();
1✔
126
      for (Method method : type.getMethods()) {
1✔
127
        if (!canHaveStatement(method)) {
1✔
128
          continue;
1✔
129
        }
130
        if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
1✔
131
            && method.getAnnotation(ResultMap.class) == null) {
1✔
132
          parseResultMap(method);
1✔
133
        }
134
        try {
135
          parseStatement(method);
1✔
136
        } catch (IncompleteElementException e) {
1✔
137
          configuration.addIncompleteMethod(new MethodResolver(this, method));
1✔
138
        }
1✔
139
      }
140
    }
141
    configuration.parsePendingMethods(false);
1✔
142
  }
1✔
143

144
  private static boolean canHaveStatement(Method method) {
145
    // issue #237
146
    return !method.isBridge() && !method.isDefault();
1✔
147
  }
148

149
  private void loadXmlResource() {
150
    // Spring may not know the real resource name so we check a flag
151
    // to prevent loading again a resource twice
152
    // this flag is set at XMLMapperBuilder#bindMapperForNamespace
153
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
1✔
154
      String xmlResource = type.getName().replace('.', '/') + ".xml";
1✔
155
      // #1347
156
      InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
1✔
157
      if (inputStream == null) {
1✔
158
        // Search XML mapper that is not in the module but in the classpath.
159
        try {
160
          inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
×
161
        } catch (IOException e2) {
1✔
162
          // ignore, resource is not required
163
        }
×
164
      }
165
      if (inputStream != null) {
1✔
166
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource,
1✔
167
            configuration.getSqlFragments(), type);
1✔
168
        xmlParser.parse();
1✔
169
      }
170
    }
171
  }
1✔
172

173
  private void parseCache() {
174
    CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
1✔
175
    if (cacheDomain != null) {
1✔
176
      Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
1!
177
      Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
1!
178
      Properties props = convertToProperties(cacheDomain.properties());
1✔
179
      assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size,
1✔
180
          cacheDomain.readWrite(), cacheDomain.blocking(), props);
1✔
181
    }
182
  }
1✔
183

184
  private Properties convertToProperties(Property[] properties) {
185
    if (properties.length == 0) {
1✔
186
      return null;
1✔
187
    }
188
    Properties props = new Properties();
1✔
189
    for (Property property : properties) {
1✔
190
      props.setProperty(property.name(), PropertyParser.parse(property.value(), configuration.getVariables()));
1✔
191
    }
192
    return props;
1✔
193
  }
194

195
  private void parseCacheRef() {
196
    CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
1✔
197
    if (cacheDomainRef != null) {
1✔
198
      Class<?> refType = cacheDomainRef.value();
1✔
199
      String refName = cacheDomainRef.name();
1✔
200
      if (refType == void.class && refName.isEmpty()) {
1✔
201
        throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
1✔
202
      }
203
      if (refType != void.class && !refName.isEmpty()) {
1✔
204
        throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
1✔
205
      }
206
      String namespace = refType != void.class ? refType.getName() : refName;
1✔
207
      try {
208
        assistant.useCacheRef(namespace);
1✔
209
      } catch (IncompleteElementException e) {
1✔
210
        configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
1✔
211
      }
1✔
212
    }
213
  }
1✔
214

215
  private String parseResultMap(Method method) {
216
    Class<?> returnType = getReturnType(method, type);
1✔
217
    Arg[] args = method.getAnnotationsByType(Arg.class);
1✔
218
    Result[] results = method.getAnnotationsByType(Result.class);
1✔
219
    TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
1✔
220
    String resultMapId = generateResultMapName(method);
1✔
221
    applyResultMap(resultMapId, returnType, args, results, typeDiscriminator);
1✔
222
    return resultMapId;
1✔
223
  }
224

225
  private String generateResultMapName(Method method) {
226
    Results results = method.getAnnotation(Results.class);
1✔
227
    if (results != null && !results.id().isEmpty()) {
1✔
228
      return type.getName() + "." + results.id();
1✔
229
    }
230
    StringBuilder suffix = new StringBuilder();
1✔
231
    for (Class<?> c : method.getParameterTypes()) {
1✔
232
      suffix.append("-");
1✔
233
      suffix.append(c.getSimpleName());
1✔
234
    }
235
    if (suffix.length() < 1) {
1✔
236
      suffix.append("-void");
1✔
237
    }
238
    return type.getName() + "." + method.getName() + suffix;
1✔
239
  }
240

241
  private void applyResultMap(String resultMapId, Class<?> returnType, Arg[] args, Result[] results,
242
      TypeDiscriminator discriminator) {
243
    List<ResultMapping> resultMappings = new ArrayList<>();
1✔
244
    applyConstructorArgs(args, returnType, resultMappings, resultMapId);
1✔
245
    applyResults(results, returnType, resultMappings);
1✔
246
    Discriminator disc = applyDiscriminator(resultMapId, returnType, discriminator);
1✔
247
    // TODO add AutoMappingBehaviour
248
    assistant.addResultMap(resultMapId, returnType, null, disc, resultMappings, null);
1✔
249
    createDiscriminatorResultMaps(resultMapId, returnType, discriminator);
1✔
250
  }
1✔
251

252
  private void createDiscriminatorResultMaps(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
253
    if (discriminator != null) {
1✔
254
      for (Case c : discriminator.cases()) {
1✔
255
        String caseResultMapId = resultMapId + "-" + c.value();
1✔
256
        List<ResultMapping> resultMappings = new ArrayList<>();
1✔
257
        // issue #136
258
        applyConstructorArgs(c.constructArgs(), resultType, resultMappings, resultMapId);
1✔
259
        applyResults(c.results(), resultType, resultMappings);
1✔
260
        // TODO add AutoMappingBehaviour
261
        assistant.addResultMap(caseResultMapId, c.type(), resultMapId, null, resultMappings, null);
1✔
262
      }
263
    }
264
  }
1✔
265

266
  private Discriminator applyDiscriminator(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
267
    if (discriminator != null) {
1✔
268
      String column = discriminator.column();
1✔
269
      Class<?> javaType = discriminator.javaType() == void.class ? String.class : discriminator.javaType();
1!
270
      JdbcType jdbcType = discriminator.jdbcType() == JdbcType.UNDEFINED ? null : discriminator.jdbcType();
1!
271
      @SuppressWarnings("unchecked")
272
      Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>) (discriminator
273
          .typeHandler() == UnknownTypeHandler.class ? null : discriminator.typeHandler());
1✔
274
      Case[] cases = discriminator.cases();
1✔
275
      Map<String, String> discriminatorMap = new HashMap<>();
1✔
276
      for (Case c : cases) {
1✔
277
        String value = c.value();
1✔
278
        String caseResultMapId = resultMapId + "-" + value;
1✔
279
        discriminatorMap.put(value, caseResultMapId);
1✔
280
      }
281
      return assistant.buildDiscriminator(resultType, column, javaType, jdbcType, typeHandler, discriminatorMap);
1✔
282
    }
283
    return null;
1✔
284
  }
285

286
  void parseStatement(Method method) {
287
    final Class<?> parameterTypeClass = getParameterType(method);
1✔
288
    final ParamNameResolver paramNameResolver = new ParamNameResolver(configuration, method, type);
1✔
289
    final LanguageDriver languageDriver = getLanguageDriver(method);
1✔
290

291
    getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> {
1✔
292
      final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass,
1✔
293
          paramNameResolver, languageDriver, method);
294
      final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType();
1✔
295
      final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options) x.getAnnotation())
1✔
296
          .orElse(null);
1✔
297
      final String mappedStatementId = type.getName() + "." + method.getName();
1✔
298

299
      final KeyGenerator keyGenerator;
300
      String keyProperty = null;
1✔
301
      String keyColumn = null;
1✔
302
      if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
1✔
303
        // first check for SelectKey annotation - that overrides everything else
304
        SelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class)
1✔
305
            .map(x -> (SelectKey) x.getAnnotation()).orElse(null);
1✔
306
        if (selectKey != null) {
1✔
307
          keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method),
1✔
308
              paramNameResolver, languageDriver);
309
          keyProperty = selectKey.keyProperty();
1✔
310
        } else if (options == null) {
1✔
311
          keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
1✔
312
        } else {
313
          keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
1✔
314
          keyProperty = options.keyProperty();
1✔
315
          keyColumn = options.keyColumn();
1✔
316
        }
317
      } else {
1✔
318
        keyGenerator = NoKeyGenerator.INSTANCE;
1✔
319
      }
320

321
      Integer fetchSize = null;
1✔
322
      Integer timeout = null;
1✔
323
      StatementType statementType = StatementType.PREPARED;
1✔
324
      ResultSetType resultSetType = configuration.getDefaultResultSetType();
1✔
325
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
1✔
326
      boolean flushCache = !isSelect;
1✔
327
      boolean useCache = isSelect;
1✔
328
      if (options != null) {
1✔
329
        if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
1✔
330
          flushCache = true;
1✔
331
        } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
1✔
332
          flushCache = false;
1✔
333
        }
334
        useCache = options.useCache();
1✔
335
        // issue #348
336
        fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null;
1✔
337
        timeout = options.timeout() > -1 ? options.timeout() : null;
1✔
338
        statementType = options.statementType();
1✔
339
        if (options.resultSetType() != ResultSetType.DEFAULT) {
1✔
340
          resultSetType = options.resultSetType();
1✔
341
        }
342
      }
343

344
      String resultMapId = null;
1✔
345
      if (isSelect) {
1✔
346
        ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
1✔
347
        if (resultMapAnnotation != null) {
1✔
348
          resultMapId = String.join(",", resultMapAnnotation.value());
1✔
349
        } else {
350
          resultMapId = generateResultMapName(method);
1✔
351
        }
352
      }
353

354
      assistant.addMappedStatement(mappedStatementId, sqlSource, statementType, sqlCommandType, fetchSize, timeout,
1✔
355
          // ParameterMapID
356
          null, parameterTypeClass, resultMapId, getReturnType(method, type), resultSetType, flushCache, useCache,
1✔
357
          // TODO gcode issue #577
358
          false, keyGenerator, keyProperty, keyColumn, statementAnnotation.getDatabaseId(), languageDriver,
1✔
359
          // ResultSets
360
          options != null ? nullOrEmpty(options.resultSets()) : null, statementAnnotation.isDirtySelect(),
1✔
361
          paramNameResolver);
362
    });
1✔
363
  }
1✔
364

365
  private LanguageDriver getLanguageDriver(Method method) {
366
    Lang lang = method.getAnnotation(Lang.class);
1✔
367
    Class<? extends LanguageDriver> langClass = null;
1✔
368
    if (lang != null) {
1✔
369
      langClass = lang.value();
1✔
370
    }
371
    return configuration.getLanguageDriver(langClass);
1✔
372
  }
373

374
  private Class<?> getParameterType(Method method) {
375
    Class<?> parameterType = null;
1✔
376
    Parameter[] parameters = method.getParameters();
1✔
377
    for (Parameter param : parameters) {
1✔
378
      Class<?> paramType = param.getType();
1✔
379
      if (RowBounds.class.isAssignableFrom(paramType) || ResultHandler.class.isAssignableFrom(paramType)) {
1✔
380
        continue;
1✔
381
      }
382
      if (parameterType == null && param.getAnnotation(Param.class) == null) {
1✔
383
        parameterType = paramType;
1✔
384
      } else {
385
        return ParamMap.class;
1✔
386
      }
387
    }
388
    return parameterType;
1✔
389
  }
390

391
  private static Class<?> getReturnType(Method method, Class<?> type) {
392
    Class<?> returnType = method.getReturnType();
1✔
393
    Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, type);
1✔
394
    if (resolvedReturnType instanceof Class) {
1✔
395
      returnType = (Class<?>) resolvedReturnType;
1✔
396
      if (returnType.isArray()) {
1✔
397
        returnType = returnType.getComponentType();
1✔
398
      }
399
      // gcode issue #508
400
      if (void.class.equals(returnType)) {
1✔
401
        ResultType rt = method.getAnnotation(ResultType.class);
1✔
402
        if (rt != null) {
1✔
403
          returnType = rt.value();
1✔
404
        }
405
      }
1✔
406
    } else if (resolvedReturnType instanceof ParameterizedType) {
1!
407
      ParameterizedType parameterizedType = (ParameterizedType) resolvedReturnType;
1✔
408
      Class<?> rawType = (Class<?>) parameterizedType.getRawType();
1✔
409
      if (Collection.class.isAssignableFrom(rawType) || Cursor.class.isAssignableFrom(rawType)) {
1✔
410
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
1✔
411
        if (actualTypeArguments != null && actualTypeArguments.length == 1) {
1!
412
          Type returnTypeParameter = actualTypeArguments[0];
1✔
413
          if (returnTypeParameter instanceof Class<?>) {
1✔
414
            returnType = (Class<?>) returnTypeParameter;
1✔
415
          } else if (returnTypeParameter instanceof ParameterizedType) {
1!
416
            // (gcode issue #443) actual type can be a also a parameterized type
417
            returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
1✔
418
          } else if (returnTypeParameter instanceof GenericArrayType) {
×
419
            Class<?> componentType = (Class<?>) ((GenericArrayType) returnTypeParameter).getGenericComponentType();
×
420
            // (gcode issue #525) support List<byte[]>
421
            returnType = Array.newInstance(componentType, 0).getClass();
×
422
          }
423
        }
424
      } else if (method.isAnnotationPresent(MapKey.class) && Map.class.isAssignableFrom(rawType)) {
1!
425
        // (gcode issue 504) Do not look into Maps if there is not MapKey annotation
426
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
1✔
427
        if (actualTypeArguments != null && actualTypeArguments.length == 2) {
1!
428
          Type returnTypeParameter = actualTypeArguments[1];
1✔
429
          if (returnTypeParameter instanceof Class<?>) {
1✔
430
            returnType = (Class<?>) returnTypeParameter;
1✔
431
          } else if (returnTypeParameter instanceof ParameterizedType) {
1!
432
            // (gcode issue 443) actual type can be a also a parameterized type
433
            returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
1✔
434
          }
435
        }
436
      } else if (Optional.class.equals(rawType)) {
1✔
437
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
1✔
438
        Type returnTypeParameter = actualTypeArguments[0];
1✔
439
        if (returnTypeParameter instanceof Class<?>) {
1!
440
          returnType = (Class<?>) returnTypeParameter;
1✔
441
        }
442
      }
443
    }
444

445
    return returnType;
1✔
446
  }
447

448
  private void applyResults(Result[] results, Class<?> resultType, List<ResultMapping> resultMappings) {
449
    for (Result result : results) {
1✔
450
      List<ResultFlag> flags = new ArrayList<>();
1✔
451
      if (result.id()) {
1✔
452
        flags.add(ResultFlag.ID);
1✔
453
      }
454
      @SuppressWarnings("unchecked")
455
      Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>) (result
456
          .typeHandler() == UnknownTypeHandler.class ? null : result.typeHandler());
1✔
457
      boolean hasNestedResultMap = hasNestedResultMap(result);
1✔
458
      ResultMapping resultMapping = assistant.buildResultMapping(resultType, nullOrEmpty(result.property()),
1✔
459
          nullOrEmpty(result.column()), result.javaType() == void.class ? null : result.javaType(),
1✔
460
          result.jdbcType() == JdbcType.UNDEFINED ? null : result.jdbcType(),
1✔
461
          hasNestedSelect(result) ? nestedSelectId(result) : null,
1✔
462
          hasNestedResultMap ? nestedResultMapId(result) : null, null,
1✔
463
          hasNestedResultMap ? findColumnPrefix(result) : null, typeHandler, flags, null, null, isLazy(result));
1✔
464
      resultMappings.add(resultMapping);
1✔
465
    }
466
  }
1✔
467

468
  private String findColumnPrefix(Result result) {
469
    String columnPrefix = result.one().columnPrefix();
1✔
470
    if (columnPrefix.isEmpty()) {
1✔
471
      columnPrefix = result.many().columnPrefix();
1✔
472
    }
473
    return columnPrefix;
1✔
474
  }
475

476
  private String nestedResultMapId(Result result) {
477
    String resultMapId = result.one().resultMap();
1✔
478
    if (resultMapId.isEmpty()) {
1✔
479
      resultMapId = result.many().resultMap();
1✔
480
    }
481
    if (!resultMapId.contains(".")) {
1✔
482
      resultMapId = type.getName() + "." + resultMapId;
1✔
483
    }
484
    return resultMapId;
1✔
485
  }
486

487
  private boolean hasNestedResultMap(Result result) {
488
    if (!result.one().resultMap().isEmpty() && !result.many().resultMap().isEmpty()) {
1!
489
      throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
×
490
    }
491
    return !result.one().resultMap().isEmpty() || !result.many().resultMap().isEmpty();
1✔
492
  }
493

494
  private String nestedSelectId(Result result) {
495
    String nestedSelect = result.one().select();
1✔
496
    if (nestedSelect.isEmpty()) {
1✔
497
      nestedSelect = result.many().select();
1✔
498
    }
499
    if (!nestedSelect.contains(".")) {
1✔
500
      nestedSelect = type.getName() + "." + nestedSelect;
1✔
501
    }
502
    return nestedSelect;
1✔
503
  }
504

505
  private boolean isLazy(Result result) {
506
    boolean isLazy = configuration.isLazyLoadingEnabled();
1✔
507
    if (!result.one().select().isEmpty() && FetchType.DEFAULT != result.one().fetchType()) {
1✔
508
      isLazy = result.one().fetchType() == FetchType.LAZY;
1!
509
    } else if (!result.many().select().isEmpty() && FetchType.DEFAULT != result.many().fetchType()) {
1✔
510
      isLazy = result.many().fetchType() == FetchType.LAZY;
1!
511
    }
512
    return isLazy;
1✔
513
  }
514

515
  private boolean hasNestedSelect(Result result) {
516
    if (!result.one().select().isEmpty() && !result.many().select().isEmpty()) {
1✔
517
      throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
1✔
518
    }
519
    return !result.one().select().isEmpty() || !result.many().select().isEmpty();
1✔
520
  }
521

522
  private void applyConstructorArgs(Arg[] args, Class<?> resultType, List<ResultMapping> resultMappings,
523
      String resultMapId) {
524
    final List<ResultMapping> mappings = new ArrayList<>();
1✔
525
    for (Arg arg : args) {
1✔
526
      List<ResultFlag> flags = new ArrayList<>();
1✔
527
      flags.add(ResultFlag.CONSTRUCTOR);
1✔
528
      if (arg.id()) {
1✔
529
        flags.add(ResultFlag.ID);
1✔
530
      }
531
      @SuppressWarnings("unchecked")
532
      Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>) (arg
533
          .typeHandler() == UnknownTypeHandler.class ? null : arg.typeHandler());
1✔
534
      ResultMapping resultMapping = assistant.buildResultMapping(resultType, nullOrEmpty(arg.name()),
1✔
535
          nullOrEmpty(arg.column()), arg.javaType() == void.class ? null : arg.javaType(),
1✔
536
          arg.jdbcType() == JdbcType.UNDEFINED ? null : arg.jdbcType(), nullOrEmpty(arg.select()),
1✔
537
          nullOrEmpty(arg.resultMap()), null, nullOrEmpty(arg.columnPrefix()), typeHandler, flags, null, null, false);
1✔
538
      mappings.add(resultMapping);
1✔
539
    }
540

541
    final ResultMappingConstructorResolver resolver = new ResultMappingConstructorResolver(configuration, mappings,
1✔
542
        resultType, resultMapId);
543
    resultMappings.addAll(resolver.resolveWithConstructor());
1✔
544
  }
1✔
545

546
  private String nullOrEmpty(String value) {
547
    return value == null || value.trim().isEmpty() ? null : value;
1!
548
  }
549

550
  private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId,
551
      Class<?> parameterTypeClass, ParamNameResolver paramNameResolver, LanguageDriver languageDriver) {
552
    String id = baseStatementId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
1✔
553
    Class<?> resultTypeClass = selectKeyAnnotation.resultType();
1✔
554
    StatementType statementType = selectKeyAnnotation.statementType();
1✔
555
    String keyProperty = selectKeyAnnotation.keyProperty();
1✔
556
    String keyColumn = selectKeyAnnotation.keyColumn();
1✔
557
    boolean executeBefore = selectKeyAnnotation.before();
1✔
558

559
    // defaults
560
    boolean useCache = false;
1✔
561
    KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
1✔
562
    Integer fetchSize = null;
1✔
563
    Integer timeout = null;
1✔
564
    boolean flushCache = false;
1✔
565
    String parameterMap = null;
1✔
566
    String resultMap = null;
1✔
567
    ResultSetType resultSetTypeEnum = null;
1✔
568
    String databaseId = selectKeyAnnotation.databaseId().isEmpty() ? null : selectKeyAnnotation.databaseId();
1✔
569

570
    SqlSource sqlSource = buildSqlSourceFromStrings(selectKeyAnnotation.statement(), parameterTypeClass,
1✔
571
        paramNameResolver, languageDriver);
572
    SqlCommandType sqlCommandType = SqlCommandType.SELECT;
1✔
573

574
    assistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
1✔
575
        parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, false, keyGenerator,
576
        keyProperty, keyColumn, databaseId, languageDriver, null, false, paramNameResolver);
577

578
    id = assistant.applyCurrentNamespace(id, false);
1✔
579

580
    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
1✔
581
    SelectKeyGenerator answer = new SelectKeyGenerator(keyStatement, executeBefore);
1✔
582
    configuration.addKeyGenerator(id, answer);
1✔
583
    return answer;
1✔
584
  }
585

586
  private SqlSource buildSqlSource(Annotation annotation, Class<?> parameterType, ParamNameResolver paramNameResolver,
587
      LanguageDriver languageDriver, Method method) {
588
    if (annotation instanceof Select) {
1✔
589
      return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, paramNameResolver, languageDriver);
1✔
590
    } else if (annotation instanceof Update) {
1✔
591
      return buildSqlSourceFromStrings(((Update) annotation).value(), parameterType, paramNameResolver, languageDriver);
1✔
592
    } else if (annotation instanceof Insert) {
1✔
593
      return buildSqlSourceFromStrings(((Insert) annotation).value(), parameterType, paramNameResolver, languageDriver);
1✔
594
    } else if (annotation instanceof Delete) {
1✔
595
      return buildSqlSourceFromStrings(((Delete) annotation).value(), parameterType, paramNameResolver, languageDriver);
1✔
596
    } else if (annotation instanceof SelectKey) {
1!
NEW
597
      return buildSqlSourceFromStrings(((SelectKey) annotation).statement(), parameterType, paramNameResolver,
×
598
          languageDriver);
599
    }
600
    return new ProviderSqlSource(assistant.getConfiguration(), annotation, type, method);
1✔
601
  }
602

603
  private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass,
604
      ParamNameResolver paramNameResolver, LanguageDriver languageDriver) {
605
    return languageDriver.createSqlSource(configuration, String.join(" ", strings).trim(), parameterTypeClass,
1✔
606
        paramNameResolver);
607
  }
608

609
  @SafeVarargs
610
  private final Optional<AnnotationWrapper> getAnnotationWrapper(Method method, boolean errorIfNoMatch,
611
      Class<? extends Annotation>... targetTypes) {
612
    return getAnnotationWrapper(method, errorIfNoMatch, Arrays.asList(targetTypes));
1✔
613
  }
614

615
  private Optional<AnnotationWrapper> getAnnotationWrapper(Method method, boolean errorIfNoMatch,
616
      Collection<Class<? extends Annotation>> targetTypes) {
617
    String databaseId = configuration.getDatabaseId();
1✔
618
    Map<String, AnnotationWrapper> statementAnnotations = targetTypes.stream()
1✔
619
        .flatMap(x -> Arrays.stream(method.getAnnotationsByType(x))).map(AnnotationWrapper::new)
1✔
620
        .collect(Collectors.toMap(AnnotationWrapper::getDatabaseId, x -> x, (existing, duplicate) -> {
1✔
621
          throw new BuilderException(
1✔
622
              String.format("Detected conflicting annotations '%s' and '%s' on '%s'.", existing.getAnnotation(),
1✔
623
                  duplicate.getAnnotation(), method.getDeclaringClass().getName() + "." + method.getName()));
1✔
624
        }));
625
    AnnotationWrapper annotationWrapper = null;
1✔
626
    if (databaseId != null) {
1✔
627
      annotationWrapper = statementAnnotations.get(databaseId);
1✔
628
    }
629
    if (annotationWrapper == null) {
1✔
630
      annotationWrapper = statementAnnotations.get("");
1✔
631
    }
632
    if (errorIfNoMatch && annotationWrapper == null && !statementAnnotations.isEmpty()) {
1✔
633
      // Annotations exist, but there is no matching one for the specified databaseId
634
      throw new BuilderException(String.format(
1✔
635
          "Could not find a statement annotation that correspond a current database or default statement on method '%s.%s'. Current database id is [%s].",
636
          method.getDeclaringClass().getName(), method.getName(), databaseId));
1✔
637
    }
638
    return Optional.ofNullable(annotationWrapper);
1✔
639
  }
640

641
  public static Class<?> getMethodReturnType(String mapperFqn, String localStatementId) {
642
    if (mapperFqn == null || localStatementId == null) {
1!
643
      return null;
×
644
    }
645
    try {
646
      Class<?> mapperClass = Resources.classForName(mapperFqn);
1✔
647
      for (Method method : mapperClass.getMethods()) {
1✔
648
        if (method.getName().equals(localStatementId) && canHaveStatement(method)) {
1!
649
          return getReturnType(method, mapperClass);
1✔
650
        }
651
      }
652
    } catch (ClassNotFoundException e) {
1✔
653
      // No corresponding mapper interface which is OK
654
    }
1✔
655
    return null;
1✔
656
  }
657

658
  private static class AnnotationWrapper {
659
    private final Annotation annotation;
660
    private final String databaseId;
661
    private final SqlCommandType sqlCommandType;
662
    private boolean dirtySelect;
663

664
    AnnotationWrapper(Annotation annotation) {
1✔
665
      this.annotation = annotation;
1✔
666
      if (annotation instanceof Select) {
1✔
667
        databaseId = ((Select) annotation).databaseId();
1✔
668
        sqlCommandType = SqlCommandType.SELECT;
1✔
669
        dirtySelect = ((Select) annotation).affectData();
1✔
670
      } else if (annotation instanceof Update) {
1✔
671
        databaseId = ((Update) annotation).databaseId();
1✔
672
        sqlCommandType = SqlCommandType.UPDATE;
1✔
673
      } else if (annotation instanceof Insert) {
1✔
674
        databaseId = ((Insert) annotation).databaseId();
1✔
675
        sqlCommandType = SqlCommandType.INSERT;
1✔
676
      } else if (annotation instanceof Delete) {
1✔
677
        databaseId = ((Delete) annotation).databaseId();
1✔
678
        sqlCommandType = SqlCommandType.DELETE;
1✔
679
      } else if (annotation instanceof SelectProvider) {
1✔
680
        databaseId = ((SelectProvider) annotation).databaseId();
1✔
681
        sqlCommandType = SqlCommandType.SELECT;
1✔
682
        dirtySelect = ((SelectProvider) annotation).affectData();
1✔
683
      } else if (annotation instanceof UpdateProvider) {
1✔
684
        databaseId = ((UpdateProvider) annotation).databaseId();
1✔
685
        sqlCommandType = SqlCommandType.UPDATE;
1✔
686
      } else if (annotation instanceof InsertProvider) {
1✔
687
        databaseId = ((InsertProvider) annotation).databaseId();
1✔
688
        sqlCommandType = SqlCommandType.INSERT;
1✔
689
      } else if (annotation instanceof DeleteProvider) {
1✔
690
        databaseId = ((DeleteProvider) annotation).databaseId();
1✔
691
        sqlCommandType = SqlCommandType.DELETE;
1✔
692
      } else {
693
        sqlCommandType = SqlCommandType.UNKNOWN;
1✔
694
        if (annotation instanceof Options) {
1✔
695
          databaseId = ((Options) annotation).databaseId();
1✔
696
        } else if (annotation instanceof SelectKey) {
1!
697
          databaseId = ((SelectKey) annotation).databaseId();
1✔
698
        } else {
699
          databaseId = "";
×
700
        }
701
      }
702
    }
1✔
703

704
    Annotation getAnnotation() {
705
      return annotation;
1✔
706
    }
707

708
    SqlCommandType getSqlCommandType() {
709
      return sqlCommandType;
1✔
710
    }
711

712
    String getDatabaseId() {
713
      return databaseId;
1✔
714
    }
715

716
    boolean isDirtySelect() {
717
      return dirtySelect;
1✔
718
    }
719
  }
720
}
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

© 2025 Coveralls, Inc