View Javadoc
1   /*
2    *    Copyright 2009-2023 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.ParameterizedType;
25  import java.lang.reflect.Type;
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.Collection;
29  import java.util.HashMap;
30  import java.util.Iterator;
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.Property;
52  import org.apache.ibatis.annotations.Result;
53  import org.apache.ibatis.annotations.ResultMap;
54  import org.apache.ibatis.annotations.ResultType;
55  import org.apache.ibatis.annotations.Results;
56  import org.apache.ibatis.annotations.Select;
57  import org.apache.ibatis.annotations.SelectKey;
58  import org.apache.ibatis.annotations.SelectProvider;
59  import org.apache.ibatis.annotations.TypeDiscriminator;
60  import org.apache.ibatis.annotations.Update;
61  import org.apache.ibatis.annotations.UpdateProvider;
62  import org.apache.ibatis.binding.MapperMethod.ParamMap;
63  import org.apache.ibatis.builder.BuilderException;
64  import org.apache.ibatis.builder.CacheRefResolver;
65  import org.apache.ibatis.builder.IncompleteElementException;
66  import org.apache.ibatis.builder.MapperBuilderAssistant;
67  import org.apache.ibatis.builder.xml.XMLMapperBuilder;
68  import org.apache.ibatis.cursor.Cursor;
69  import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
70  import org.apache.ibatis.executor.keygen.KeyGenerator;
71  import org.apache.ibatis.executor.keygen.NoKeyGenerator;
72  import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
73  import org.apache.ibatis.io.Resources;
74  import org.apache.ibatis.mapping.Discriminator;
75  import org.apache.ibatis.mapping.FetchType;
76  import org.apache.ibatis.mapping.MappedStatement;
77  import org.apache.ibatis.mapping.ResultFlag;
78  import org.apache.ibatis.mapping.ResultMapping;
79  import org.apache.ibatis.mapping.ResultSetType;
80  import org.apache.ibatis.mapping.SqlCommandType;
81  import org.apache.ibatis.mapping.SqlSource;
82  import org.apache.ibatis.mapping.StatementType;
83  import org.apache.ibatis.parsing.PropertyParser;
84  import org.apache.ibatis.reflection.TypeParameterResolver;
85  import org.apache.ibatis.scripting.LanguageDriver;
86  import org.apache.ibatis.session.Configuration;
87  import org.apache.ibatis.session.ResultHandler;
88  import org.apache.ibatis.session.RowBounds;
89  import org.apache.ibatis.type.JdbcType;
90  import org.apache.ibatis.type.TypeHandler;
91  import org.apache.ibatis.type.UnknownTypeHandler;
92  
93  /**
94   * @author Clinton Begin
95   * @author Kazuki Shimizu
96   */
97  public class MapperAnnotationBuilder {
98  
99    private static final Set<Class<? extends Annotation>> statementAnnotationTypes = Stream
100       .of(Select.class, Update.class, Insert.class, Delete.class, SelectProvider.class, UpdateProvider.class,
101           InsertProvider.class, DeleteProvider.class)
102       .collect(Collectors.toSet());
103 
104   private final Configuration configuration;
105   private final MapperBuilderAssistant assistant;
106   private final Class<?> type;
107 
108   public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
109     String resource = type.getName().replace('.', '/') + ".java (best guess)";
110     this.assistant = new MapperBuilderAssistant(configuration, resource);
111     this.configuration = configuration;
112     this.type = type;
113   }
114 
115   public void parse() {
116     String resource = type.toString();
117     if (!configuration.isResourceLoaded(resource)) {
118       loadXmlResource();
119       configuration.addLoadedResource(resource);
120       assistant.setCurrentNamespace(type.getName());
121       parseCache();
122       parseCacheRef();
123       for (Method method : type.getMethods()) {
124         if (!canHaveStatement(method)) {
125           continue;
126         }
127         if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
128             && method.getAnnotation(ResultMap.class) == null) {
129           parseResultMap(method);
130         }
131         try {
132           parseStatement(method);
133         } catch (IncompleteElementException e) {
134           configuration.addIncompleteMethod(new MethodResolver(this, method));
135         }
136       }
137     }
138     parsePendingMethods();
139   }
140 
141   private static boolean canHaveStatement(Method method) {
142     // issue #237
143     return !method.isBridge() && !method.isDefault();
144   }
145 
146   private void parsePendingMethods() {
147     Collection<MethodResolver> incompleteMethods = configuration.getIncompleteMethods();
148     synchronized (incompleteMethods) {
149       Iterator<MethodResolver> iter = incompleteMethods.iterator();
150       while (iter.hasNext()) {
151         try {
152           iter.next().resolve();
153           iter.remove();
154         } catch (IncompleteElementException e) {
155           // This method is still missing a resource
156         }
157       }
158     }
159   }
160 
161   private void loadXmlResource() {
162     // Spring may not know the real resource name so we check a flag
163     // to prevent loading again a resource twice
164     // this flag is set at XMLMapperBuilder#bindMapperForNamespace
165     if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
166       String xmlResource = type.getName().replace('.', '/') + ".xml";
167       // #1347
168       InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
169       if (inputStream == null) {
170         // Search XML mapper that is not in the module but in the classpath.
171         try {
172           inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
173         } catch (IOException e2) {
174           // ignore, resource is not required
175         }
176       }
177       if (inputStream != null) {
178         XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource,
179             configuration.getSqlFragments(), type.getName());
180         xmlParser.parse();
181       }
182     }
183   }
184 
185   private void parseCache() {
186     CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
187     if (cacheDomain != null) {
188       Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
189       Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
190       Properties props = convertToProperties(cacheDomain.properties());
191       assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size,
192           cacheDomain.readWrite(), cacheDomain.blocking(), props);
193     }
194   }
195 
196   private Properties convertToProperties(Property[] properties) {
197     if (properties.length == 0) {
198       return null;
199     }
200     Properties props = new Properties();
201     for (Property property : properties) {
202       props.setProperty(property.name(), PropertyParser.parse(property.value(), configuration.getVariables()));
203     }
204     return props;
205   }
206 
207   private void parseCacheRef() {
208     CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
209     if (cacheDomainRef != null) {
210       Class<?> refType = cacheDomainRef.value();
211       String refName = cacheDomainRef.name();
212       if (refType == void.class && refName.isEmpty()) {
213         throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
214       }
215       if (refType != void.class && !refName.isEmpty()) {
216         throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
217       }
218       String namespace = refType != void.class ? refType.getName() : refName;
219       try {
220         assistant.useCacheRef(namespace);
221       } catch (IncompleteElementException e) {
222         configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
223       }
224     }
225   }
226 
227   private String parseResultMap(Method method) {
228     Class<?> returnType = getReturnType(method, type);
229     Arg[] args = method.getAnnotationsByType(Arg.class);
230     Result[] results = method.getAnnotationsByType(Result.class);
231     TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
232     String resultMapId = generateResultMapName(method);
233     applyResultMap(resultMapId, returnType, args, results, typeDiscriminator);
234     return resultMapId;
235   }
236 
237   private String generateResultMapName(Method method) {
238     Results results = method.getAnnotation(Results.class);
239     if (results != null && !results.id().isEmpty()) {
240       return type.getName() + "." + results.id();
241     }
242     StringBuilder suffix = new StringBuilder();
243     for (Class<?> c : method.getParameterTypes()) {
244       suffix.append("-");
245       suffix.append(c.getSimpleName());
246     }
247     if (suffix.length() < 1) {
248       suffix.append("-void");
249     }
250     return type.getName() + "." + method.getName() + suffix;
251   }
252 
253   private void applyResultMap(String resultMapId, Class<?> returnType, Arg[] args, Result[] results,
254       TypeDiscriminator discriminator) {
255     List<ResultMapping> resultMappings = new ArrayList<>();
256     applyConstructorArgs(args, returnType, resultMappings);
257     applyResults(results, returnType, resultMappings);
258     Discriminator disc = applyDiscriminator(resultMapId, returnType, discriminator);
259     // TODO add AutoMappingBehaviour
260     assistant.addResultMap(resultMapId, returnType, null, disc, resultMappings, null);
261     createDiscriminatorResultMaps(resultMapId, returnType, discriminator);
262   }
263 
264   private void createDiscriminatorResultMaps(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
265     if (discriminator != null) {
266       for (Case c : discriminator.cases()) {
267         String caseResultMapId = resultMapId + "-" + c.value();
268         List<ResultMapping> resultMappings = new ArrayList<>();
269         // issue #136
270         applyConstructorArgs(c.constructArgs(), resultType, resultMappings);
271         applyResults(c.results(), resultType, resultMappings);
272         // TODO add AutoMappingBehaviour
273         assistant.addResultMap(caseResultMapId, c.type(), resultMapId, null, resultMappings, null);
274       }
275     }
276   }
277 
278   private Discriminator applyDiscriminator(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
279     if (discriminator != null) {
280       String column = discriminator.column();
281       Class<?> javaType = discriminator.javaType() == void.class ? String.class : discriminator.javaType();
282       JdbcType jdbcType = discriminator.jdbcType() == JdbcType.UNDEFINED ? null : discriminator.jdbcType();
283       @SuppressWarnings("unchecked")
284       Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>) (discriminator
285           .typeHandler() == UnknownTypeHandler.class ? null : discriminator.typeHandler());
286       Case[] cases = discriminator.cases();
287       Map<String, String> discriminatorMap = new HashMap<>();
288       for (Case c : cases) {
289         String value = c.value();
290         String caseResultMapId = resultMapId + "-" + value;
291         discriminatorMap.put(value, caseResultMapId);
292       }
293       return assistant.buildDiscriminator(resultType, column, javaType, jdbcType, typeHandler, discriminatorMap);
294     }
295     return null;
296   }
297 
298   void parseStatement(Method method) {
299     final Class<?> parameterTypeClass = getParameterType(method);
300     final LanguageDriver languageDriver = getLanguageDriver(method);
301 
302     getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> {
303       final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass,
304           languageDriver, method);
305       final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType();
306       final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options) x.getAnnotation())
307           .orElse(null);
308       final String mappedStatementId = type.getName() + "." + method.getName();
309 
310       final KeyGenerator keyGenerator;
311       String keyProperty = null;
312       String keyColumn = null;
313       if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
314         // first check for SelectKey annotation - that overrides everything else
315         SelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class)
316             .map(x -> (SelectKey) x.getAnnotation()).orElse(null);
317         if (selectKey != null) {
318           keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method),
319               languageDriver);
320           keyProperty = selectKey.keyProperty();
321         } else if (options == null) {
322           keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
323         } else {
324           keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
325           keyProperty = options.keyProperty();
326           keyColumn = options.keyColumn();
327         }
328       } else {
329         keyGenerator = NoKeyGenerator.INSTANCE;
330       }
331 
332       Integer fetchSize = null;
333       Integer timeout = null;
334       StatementType statementType = StatementType.PREPARED;
335       ResultSetType resultSetType = configuration.getDefaultResultSetType();
336       boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
337       boolean flushCache = !isSelect;
338       boolean useCache = isSelect;
339       if (options != null) {
340         if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
341           flushCache = true;
342         } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
343           flushCache = false;
344         }
345         useCache = options.useCache();
346         // issue #348
347         fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null;
348         timeout = options.timeout() > -1 ? options.timeout() : null;
349         statementType = options.statementType();
350         if (options.resultSetType() != ResultSetType.DEFAULT) {
351           resultSetType = options.resultSetType();
352         }
353       }
354 
355       String resultMapId = null;
356       if (isSelect) {
357         ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
358         if (resultMapAnnotation != null) {
359           resultMapId = String.join(",", resultMapAnnotation.value());
360         } else {
361           resultMapId = generateResultMapName(method);
362         }
363       }
364 
365       assistant.addMappedStatement(mappedStatementId, sqlSource, statementType, sqlCommandType, fetchSize, timeout,
366           // ParameterMapID
367           null, parameterTypeClass, resultMapId, getReturnType(method, type), resultSetType, flushCache, useCache,
368           // TODO gcode issue #577
369           false, keyGenerator, keyProperty, keyColumn, statementAnnotation.getDatabaseId(), languageDriver,
370           // ResultSets
371           options != null ? nullOrEmpty(options.resultSets()) : null, statementAnnotation.isDirtySelect());
372     });
373   }
374 
375   private LanguageDriver getLanguageDriver(Method method) {
376     Lang lang = method.getAnnotation(Lang.class);
377     Class<? extends LanguageDriver> langClass = null;
378     if (lang != null) {
379       langClass = lang.value();
380     }
381     return configuration.getLanguageDriver(langClass);
382   }
383 
384   private Class<?> getParameterType(Method method) {
385     Class<?> parameterType = null;
386     Class<?>[] parameterTypes = method.getParameterTypes();
387     for (Class<?> currentParameterType : parameterTypes) {
388       if (!RowBounds.class.isAssignableFrom(currentParameterType)
389           && !ResultHandler.class.isAssignableFrom(currentParameterType)) {
390         if (parameterType == null) {
391           parameterType = currentParameterType;
392         } else {
393           // issue #135
394           parameterType = ParamMap.class;
395         }
396       }
397     }
398     return parameterType;
399   }
400 
401   private static Class<?> getReturnType(Method method, Class<?> type) {
402     Class<?> returnType = method.getReturnType();
403     Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, type);
404     if (resolvedReturnType instanceof Class) {
405       returnType = (Class<?>) resolvedReturnType;
406       if (returnType.isArray()) {
407         returnType = returnType.getComponentType();
408       }
409       // gcode issue #508
410       if (void.class.equals(returnType)) {
411         ResultType rt = method.getAnnotation(ResultType.class);
412         if (rt != null) {
413           returnType = rt.value();
414         }
415       }
416     } else if (resolvedReturnType instanceof ParameterizedType) {
417       ParameterizedType parameterizedType = (ParameterizedType) resolvedReturnType;
418       Class<?> rawType = (Class<?>) parameterizedType.getRawType();
419       if (Collection.class.isAssignableFrom(rawType) || Cursor.class.isAssignableFrom(rawType)) {
420         Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
421         if (actualTypeArguments != null && actualTypeArguments.length == 1) {
422           Type returnTypeParameter = actualTypeArguments[0];
423           if (returnTypeParameter instanceof Class<?>) {
424             returnType = (Class<?>) returnTypeParameter;
425           } else if (returnTypeParameter instanceof ParameterizedType) {
426             // (gcode issue #443) actual type can be a also a parameterized type
427             returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
428           } else if (returnTypeParameter instanceof GenericArrayType) {
429             Class<?> componentType = (Class<?>) ((GenericArrayType) returnTypeParameter).getGenericComponentType();
430             // (gcode issue #525) support List<byte[]>
431             returnType = Array.newInstance(componentType, 0).getClass();
432           }
433         }
434       } else if (method.isAnnotationPresent(MapKey.class) && Map.class.isAssignableFrom(rawType)) {
435         // (gcode issue 504) Do not look into Maps if there is not MapKey annotation
436         Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
437         if (actualTypeArguments != null && actualTypeArguments.length == 2) {
438           Type returnTypeParameter = actualTypeArguments[1];
439           if (returnTypeParameter instanceof Class<?>) {
440             returnType = (Class<?>) returnTypeParameter;
441           } else if (returnTypeParameter instanceof ParameterizedType) {
442             // (gcode issue 443) actual type can be a also a parameterized type
443             returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
444           }
445         }
446       } else if (Optional.class.equals(rawType)) {
447         Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
448         Type returnTypeParameter = actualTypeArguments[0];
449         if (returnTypeParameter instanceof Class<?>) {
450           returnType = (Class<?>) returnTypeParameter;
451         }
452       }
453     }
454 
455     return returnType;
456   }
457 
458   private void applyResults(Result[] results, Class<?> resultType, List<ResultMapping> resultMappings) {
459     for (Result result : results) {
460       List<ResultFlag> flags = new ArrayList<>();
461       if (result.id()) {
462         flags.add(ResultFlag.ID);
463       }
464       @SuppressWarnings("unchecked")
465       Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>) (result
466           .typeHandler() == UnknownTypeHandler.class ? null : result.typeHandler());
467       boolean hasNestedResultMap = hasNestedResultMap(result);
468       ResultMapping resultMapping = assistant.buildResultMapping(resultType, nullOrEmpty(result.property()),
469           nullOrEmpty(result.column()), result.javaType() == void.class ? null : result.javaType(),
470           result.jdbcType() == JdbcType.UNDEFINED ? null : result.jdbcType(),
471           hasNestedSelect(result) ? nestedSelectId(result) : null,
472           hasNestedResultMap ? nestedResultMapId(result) : null, null,
473           hasNestedResultMap ? findColumnPrefix(result) : null, typeHandler, flags, null, null, isLazy(result));
474       resultMappings.add(resultMapping);
475     }
476   }
477 
478   private String findColumnPrefix(Result result) {
479     String columnPrefix = result.one().columnPrefix();
480     if (columnPrefix.length() < 1) {
481       columnPrefix = result.many().columnPrefix();
482     }
483     return columnPrefix;
484   }
485 
486   private String nestedResultMapId(Result result) {
487     String resultMapId = result.one().resultMap();
488     if (resultMapId.length() < 1) {
489       resultMapId = result.many().resultMap();
490     }
491     if (!resultMapId.contains(".")) {
492       resultMapId = type.getName() + "." + resultMapId;
493     }
494     return resultMapId;
495   }
496 
497   private boolean hasNestedResultMap(Result result) {
498     if (result.one().resultMap().length() > 0 && result.many().resultMap().length() > 0) {
499       throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
500     }
501     return result.one().resultMap().length() > 0 || result.many().resultMap().length() > 0;
502   }
503 
504   private String nestedSelectId(Result result) {
505     String nestedSelect = result.one().select();
506     if (nestedSelect.length() < 1) {
507       nestedSelect = result.many().select();
508     }
509     if (!nestedSelect.contains(".")) {
510       nestedSelect = type.getName() + "." + nestedSelect;
511     }
512     return nestedSelect;
513   }
514 
515   private boolean isLazy(Result result) {
516     boolean isLazy = configuration.isLazyLoadingEnabled();
517     if (result.one().select().length() > 0 && FetchType.DEFAULT != result.one().fetchType()) {
518       isLazy = result.one().fetchType() == FetchType.LAZY;
519     } else if (result.many().select().length() > 0 && FetchType.DEFAULT != result.many().fetchType()) {
520       isLazy = result.many().fetchType() == FetchType.LAZY;
521     }
522     return isLazy;
523   }
524 
525   private boolean hasNestedSelect(Result result) {
526     if (result.one().select().length() > 0 && result.many().select().length() > 0) {
527       throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
528     }
529     return result.one().select().length() > 0 || result.many().select().length() > 0;
530   }
531 
532   private void applyConstructorArgs(Arg[] args, Class<?> resultType, List<ResultMapping> resultMappings) {
533     for (Arg arg : args) {
534       List<ResultFlag> flags = new ArrayList<>();
535       flags.add(ResultFlag.CONSTRUCTOR);
536       if (arg.id()) {
537         flags.add(ResultFlag.ID);
538       }
539       @SuppressWarnings("unchecked")
540       Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>) (arg
541           .typeHandler() == UnknownTypeHandler.class ? null : arg.typeHandler());
542       ResultMapping resultMapping = assistant.buildResultMapping(resultType, nullOrEmpty(arg.name()),
543           nullOrEmpty(arg.column()), arg.javaType() == void.class ? null : arg.javaType(),
544           arg.jdbcType() == JdbcType.UNDEFINED ? null : arg.jdbcType(), nullOrEmpty(arg.select()),
545           nullOrEmpty(arg.resultMap()), null, nullOrEmpty(arg.columnPrefix()), typeHandler, flags, null, null, false);
546       resultMappings.add(resultMapping);
547     }
548   }
549 
550   private String nullOrEmpty(String value) {
551     return value == null || value.trim().length() == 0 ? null : value;
552   }
553 
554   private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId,
555       Class<?> parameterTypeClass, LanguageDriver languageDriver) {
556     String id = baseStatementId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
557     Class<?> resultTypeClass = selectKeyAnnotation.resultType();
558     StatementType statementType = selectKeyAnnotation.statementType();
559     String keyProperty = selectKeyAnnotation.keyProperty();
560     String keyColumn = selectKeyAnnotation.keyColumn();
561     boolean executeBefore = selectKeyAnnotation.before();
562 
563     // defaults
564     boolean useCache = false;
565     KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
566     Integer fetchSize = null;
567     Integer timeout = null;
568     boolean flushCache = false;
569     String parameterMap = null;
570     String resultMap = null;
571     ResultSetType resultSetTypeEnum = null;
572     String databaseId = selectKeyAnnotation.databaseId().isEmpty() ? null : selectKeyAnnotation.databaseId();
573 
574     SqlSource sqlSource = buildSqlSource(selectKeyAnnotation, parameterTypeClass, languageDriver, null);
575     SqlCommandType sqlCommandType = SqlCommandType.SELECT;
576 
577     assistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
578         parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, false, keyGenerator,
579         keyProperty, keyColumn, databaseId, languageDriver, null, false);
580 
581     id = assistant.applyCurrentNamespace(id, false);
582 
583     MappedStatement keyStatement = configuration.getMappedStatement(id, false);
584     SelectKeyGenerator answer = new SelectKeyGenerator(keyStatement, executeBefore);
585     configuration.addKeyGenerator(id, answer);
586     return answer;
587   }
588 
589   private SqlSource buildSqlSource(Annotation annotation, Class<?> parameterType, LanguageDriver languageDriver,
590       Method method) {
591     if (annotation instanceof Select) {
592       return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, languageDriver);
593     }
594     if (annotation instanceof Update) {
595       return buildSqlSourceFromStrings(((Update) annotation).value(), parameterType, languageDriver);
596     } else if (annotation instanceof Insert) {
597       return buildSqlSourceFromStrings(((Insert) annotation).value(), parameterType, languageDriver);
598     } else if (annotation instanceof Delete) {
599       return buildSqlSourceFromStrings(((Delete) annotation).value(), parameterType, languageDriver);
600     } else if (annotation instanceof SelectKey) {
601       return buildSqlSourceFromStrings(((SelectKey) annotation).statement(), parameterType, languageDriver);
602     }
603     return new ProviderSqlSource(assistant.getConfiguration(), annotation, type, method);
604   }
605 
606   private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass,
607       LanguageDriver languageDriver) {
608     return languageDriver.createSqlSource(configuration, String.join(" ", strings).trim(), parameterTypeClass);
609   }
610 
611   @SafeVarargs
612   private final Optional<AnnotationWrapper> getAnnotationWrapper(Method method, boolean errorIfNoMatch,
613       Class<? extends Annotation>... targetTypes) {
614     return getAnnotationWrapper(method, errorIfNoMatch, Arrays.asList(targetTypes));
615   }
616 
617   private Optional<AnnotationWrapper> getAnnotationWrapper(Method method, boolean errorIfNoMatch,
618       Collection<Class<? extends Annotation>> targetTypes) {
619     String databaseId = configuration.getDatabaseId();
620     Map<String, AnnotationWrapper> statementAnnotations = targetTypes.stream()
621         .flatMap(x -> Arrays.stream(method.getAnnotationsByType(x))).map(AnnotationWrapper::new)
622         .collect(Collectors.toMap(AnnotationWrapper::getDatabaseId, x -> x, (existing, duplicate) -> {
623           throw new BuilderException(
624               String.format("Detected conflicting annotations '%s' and '%s' on '%s'.", existing.getAnnotation(),
625                   duplicate.getAnnotation(), method.getDeclaringClass().getName() + "." + method.getName()));
626         }));
627     AnnotationWrapper annotationWrapper = null;
628     if (databaseId != null) {
629       annotationWrapper = statementAnnotations.get(databaseId);
630     }
631     if (annotationWrapper == null) {
632       annotationWrapper = statementAnnotations.get("");
633     }
634     if (errorIfNoMatch && annotationWrapper == null && !statementAnnotations.isEmpty()) {
635       // Annotations exist, but there is no matching one for the specified databaseId
636       throw new BuilderException(String.format(
637           "Could not find a statement annotation that correspond a current database or default statement on method '%s.%s'. Current database id is [%s].",
638           method.getDeclaringClass().getName(), method.getName(), databaseId));
639     }
640     return Optional.ofNullable(annotationWrapper);
641   }
642 
643   public static Class<?> getMethodReturnType(String mapperFqn, String localStatementId) {
644     if (mapperFqn == null || localStatementId == null) {
645       return null;
646     }
647     try {
648       Class<?> mapperClass = Resources.classForName(mapperFqn);
649       for (Method method : mapperClass.getMethods()) {
650         if (method.getName().equals(localStatementId) && canHaveStatement(method)) {
651           return getReturnType(method, mapperClass);
652         }
653       }
654     } catch (ClassNotFoundException e) {
655       // No corresponding mapper interface which is OK
656     }
657     return null;
658   }
659 
660   private static class AnnotationWrapper {
661     private final Annotation annotation;
662     private final String databaseId;
663     private final SqlCommandType sqlCommandType;
664     private boolean dirtySelect;
665 
666     AnnotationWrapper(Annotation annotation) {
667       this.annotation = annotation;
668       if (annotation instanceof Select) {
669         databaseId = ((Select) annotation).databaseId();
670         sqlCommandType = SqlCommandType.SELECT;
671         dirtySelect = ((Select) annotation).affectData();
672       } else if (annotation instanceof Update) {
673         databaseId = ((Update) annotation).databaseId();
674         sqlCommandType = SqlCommandType.UPDATE;
675       } else if (annotation instanceof Insert) {
676         databaseId = ((Insert) annotation).databaseId();
677         sqlCommandType = SqlCommandType.INSERT;
678       } else if (annotation instanceof Delete) {
679         databaseId = ((Delete) annotation).databaseId();
680         sqlCommandType = SqlCommandType.DELETE;
681       } else if (annotation instanceof SelectProvider) {
682         databaseId = ((SelectProvider) annotation).databaseId();
683         sqlCommandType = SqlCommandType.SELECT;
684         dirtySelect = ((SelectProvider) annotation).affectData();
685       } else if (annotation instanceof UpdateProvider) {
686         databaseId = ((UpdateProvider) annotation).databaseId();
687         sqlCommandType = SqlCommandType.UPDATE;
688       } else if (annotation instanceof InsertProvider) {
689         databaseId = ((InsertProvider) annotation).databaseId();
690         sqlCommandType = SqlCommandType.INSERT;
691       } else if (annotation instanceof DeleteProvider) {
692         databaseId = ((DeleteProvider) annotation).databaseId();
693         sqlCommandType = SqlCommandType.DELETE;
694       } else {
695         sqlCommandType = SqlCommandType.UNKNOWN;
696         if (annotation instanceof Options) {
697           databaseId = ((Options) annotation).databaseId();
698         } else if (annotation instanceof SelectKey) {
699           databaseId = ((SelectKey) annotation).databaseId();
700         } else {
701           databaseId = "";
702         }
703       }
704     }
705 
706     Annotation getAnnotation() {
707       return annotation;
708     }
709 
710     SqlCommandType getSqlCommandType() {
711       return sqlCommandType;
712     }
713 
714     String getDatabaseId() {
715       return databaseId;
716     }
717 
718     boolean isDirtySelect() {
719       return dirtySelect;
720     }
721   }
722 }