View Javadoc
1   /*
2    *    Copyright 2009-2021 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    *       http://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 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, configuration.getSqlFragments(), type.getName());
179         xmlParser.parse();
180       }
181     }
182   }
183 
184   private void parseCache() {
185     CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
186     if (cacheDomain != null) {
187       Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
188       Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
189       Properties props = convertToProperties(cacheDomain.properties());
190       assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
191     }
192   }
193 
194   private Properties convertToProperties(Property[] properties) {
195     if (properties.length == 0) {
196       return null;
197     }
198     Properties props = new Properties();
199     for (Property property : properties) {
200       props.setProperty(property.name(),
201           PropertyParser.parse(property.value(), configuration.getVariables()));
202     }
203     return props;
204   }
205 
206   private void parseCacheRef() {
207     CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
208     if (cacheDomainRef != null) {
209       Class<?> refType = cacheDomainRef.value();
210       String refName = cacheDomainRef.name();
211       if (refType == void.class && refName.isEmpty()) {
212         throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
213       }
214       if (refType != void.class && !refName.isEmpty()) {
215         throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
216       }
217       String namespace = (refType != void.class) ? refType.getName() : refName;
218       try {
219         assistant.useCacheRef(namespace);
220       } catch (IncompleteElementException e) {
221         configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
222       }
223     }
224   }
225 
226   private String parseResultMap(Method method) {
227     Class<?> returnType = getReturnType(method);
228     Arg[] args = method.getAnnotationsByType(Arg.class);
229     Result[] results = method.getAnnotationsByType(Result.class);
230     TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
231     String resultMapId = generateResultMapName(method);
232     applyResultMap(resultMapId, returnType, args, results, typeDiscriminator);
233     return resultMapId;
234   }
235 
236   private String generateResultMapName(Method method) {
237     Results results = method.getAnnotation(Results.class);
238     if (results != null && !results.id().isEmpty()) {
239       return type.getName() + "." + results.id();
240     }
241     StringBuilder suffix = new StringBuilder();
242     for (Class<?> c : method.getParameterTypes()) {
243       suffix.append("-");
244       suffix.append(c.getSimpleName());
245     }
246     if (suffix.length() < 1) {
247       suffix.append("-void");
248     }
249     return type.getName() + "." + method.getName() + suffix;
250   }
251 
252   private void applyResultMap(String resultMapId, Class<?> returnType, Arg[] args, Result[] results, TypeDiscriminator discriminator) {
253     List<ResultMapping> resultMappings = new ArrayList<>();
254     applyConstructorArgs(args, returnType, resultMappings);
255     applyResults(results, returnType, resultMappings);
256     Discriminator disc = applyDiscriminator(resultMapId, returnType, discriminator);
257     // TODO add AutoMappingBehaviour
258     assistant.addResultMap(resultMapId, returnType, null, disc, resultMappings, null);
259     createDiscriminatorResultMaps(resultMapId, returnType, discriminator);
260   }
261 
262   private void createDiscriminatorResultMaps(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
263     if (discriminator != null) {
264       for (Case c : discriminator.cases()) {
265         String caseResultMapId = resultMapId + "-" + c.value();
266         List<ResultMapping> resultMappings = new ArrayList<>();
267         // issue #136
268         applyConstructorArgs(c.constructArgs(), resultType, resultMappings);
269         applyResults(c.results(), resultType, resultMappings);
270         // TODO add AutoMappingBehaviour
271         assistant.addResultMap(caseResultMapId, c.type(), resultMapId, null, resultMappings, null);
272       }
273     }
274   }
275 
276   private Discriminator applyDiscriminator(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
277     if (discriminator != null) {
278       String column = discriminator.column();
279       Class<?> javaType = discriminator.javaType() == void.class ? String.class : discriminator.javaType();
280       JdbcType jdbcType = discriminator.jdbcType() == JdbcType.UNDEFINED ? null : discriminator.jdbcType();
281       @SuppressWarnings("unchecked")
282       Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>)
283               (discriminator.typeHandler() == UnknownTypeHandler.class ? null : discriminator.typeHandler());
284       Case[] cases = discriminator.cases();
285       Map<String, String> discriminatorMap = new HashMap<>();
286       for (Case c : cases) {
287         String value = c.value();
288         String caseResultMapId = resultMapId + "-" + value;
289         discriminatorMap.put(value, caseResultMapId);
290       }
291       return assistant.buildDiscriminator(resultType, column, javaType, jdbcType, typeHandler, discriminatorMap);
292     }
293     return null;
294   }
295 
296   void parseStatement(Method method) {
297     final Class<?> parameterTypeClass = getParameterType(method);
298     final LanguageDriver languageDriver = getLanguageDriver(method);
299 
300     getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> {
301       final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass, languageDriver, method);
302       final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType();
303       final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options)x.getAnnotation()).orElse(null);
304       final String mappedStatementId = type.getName() + "." + method.getName();
305 
306       final KeyGenerator keyGenerator;
307       String keyProperty = null;
308       String keyColumn = null;
309       if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
310         // first check for SelectKey annotation - that overrides everything else
311         SelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class).map(x -> (SelectKey)x.getAnnotation()).orElse(null);
312         if (selectKey != null) {
313           keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
314           keyProperty = selectKey.keyProperty();
315         } else if (options == null) {
316           keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
317         } else {
318           keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
319           keyProperty = options.keyProperty();
320           keyColumn = options.keyColumn();
321         }
322       } else {
323         keyGenerator = NoKeyGenerator.INSTANCE;
324       }
325 
326       Integer fetchSize = null;
327       Integer timeout = null;
328       StatementType statementType = StatementType.PREPARED;
329       ResultSetType resultSetType = configuration.getDefaultResultSetType();
330       boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
331       boolean flushCache = !isSelect;
332       boolean useCache = isSelect;
333       if (options != null) {
334         if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
335           flushCache = true;
336         } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
337           flushCache = false;
338         }
339         useCache = options.useCache();
340         fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
341         timeout = options.timeout() > -1 ? options.timeout() : null;
342         statementType = options.statementType();
343         if (options.resultSetType() != ResultSetType.DEFAULT) {
344           resultSetType = options.resultSetType();
345         }
346       }
347 
348       String resultMapId = null;
349       if (isSelect) {
350         ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
351         if (resultMapAnnotation != null) {
352           resultMapId = String.join(",", resultMapAnnotation.value());
353         } else {
354           resultMapId = generateResultMapName(method);
355         }
356       }
357 
358       assistant.addMappedStatement(
359           mappedStatementId,
360           sqlSource,
361           statementType,
362           sqlCommandType,
363           fetchSize,
364           timeout,
365           // ParameterMapID
366           null,
367           parameterTypeClass,
368           resultMapId,
369           getReturnType(method),
370           resultSetType,
371           flushCache,
372           useCache,
373           // TODO gcode issue #577
374           false,
375           keyGenerator,
376           keyProperty,
377           keyColumn,
378           statementAnnotation.getDatabaseId(),
379           languageDriver,
380           // ResultSets
381           options != null ? nullOrEmpty(options.resultSets()) : null);
382     });
383   }
384 
385   private LanguageDriver getLanguageDriver(Method method) {
386     Lang lang = method.getAnnotation(Lang.class);
387     Class<? extends LanguageDriver> langClass = null;
388     if (lang != null) {
389       langClass = lang.value();
390     }
391     return configuration.getLanguageDriver(langClass);
392   }
393 
394   private Class<?> getParameterType(Method method) {
395     Class<?> parameterType = null;
396     Class<?>[] parameterTypes = method.getParameterTypes();
397     for (Class<?> currentParameterType : parameterTypes) {
398       if (!RowBounds.class.isAssignableFrom(currentParameterType) && !ResultHandler.class.isAssignableFrom(currentParameterType)) {
399         if (parameterType == null) {
400           parameterType = currentParameterType;
401         } else {
402           // issue #135
403           parameterType = ParamMap.class;
404         }
405       }
406     }
407     return parameterType;
408   }
409 
410   private Class<?> getReturnType(Method method) {
411     Class<?> returnType = method.getReturnType();
412     Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, type);
413     if (resolvedReturnType instanceof Class) {
414       returnType = (Class<?>) resolvedReturnType;
415       if (returnType.isArray()) {
416         returnType = returnType.getComponentType();
417       }
418       // gcode issue #508
419       if (void.class.equals(returnType)) {
420         ResultType rt = method.getAnnotation(ResultType.class);
421         if (rt != null) {
422           returnType = rt.value();
423         }
424       }
425     } else if (resolvedReturnType instanceof ParameterizedType) {
426       ParameterizedType parameterizedType = (ParameterizedType) resolvedReturnType;
427       Class<?> rawType = (Class<?>) parameterizedType.getRawType();
428       if (Collection.class.isAssignableFrom(rawType) || Cursor.class.isAssignableFrom(rawType)) {
429         Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
430         if (actualTypeArguments != null && actualTypeArguments.length == 1) {
431           Type returnTypeParameter = actualTypeArguments[0];
432           if (returnTypeParameter instanceof Class<?>) {
433             returnType = (Class<?>) returnTypeParameter;
434           } else if (returnTypeParameter instanceof ParameterizedType) {
435             // (gcode issue #443) actual type can be a also a parameterized type
436             returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
437           } else if (returnTypeParameter instanceof GenericArrayType) {
438             Class<?> componentType = (Class<?>) ((GenericArrayType) returnTypeParameter).getGenericComponentType();
439             // (gcode issue #525) support List<byte[]>
440             returnType = Array.newInstance(componentType, 0).getClass();
441           }
442         }
443       } else if (method.isAnnotationPresent(MapKey.class) && Map.class.isAssignableFrom(rawType)) {
444         // (gcode issue 504) Do not look into Maps if there is not MapKey annotation
445         Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
446         if (actualTypeArguments != null && actualTypeArguments.length == 2) {
447           Type returnTypeParameter = actualTypeArguments[1];
448           if (returnTypeParameter instanceof Class<?>) {
449             returnType = (Class<?>) returnTypeParameter;
450           } else if (returnTypeParameter instanceof ParameterizedType) {
451             // (gcode issue 443) actual type can be a also a parameterized type
452             returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
453           }
454         }
455       } else if (Optional.class.equals(rawType)) {
456         Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
457         Type returnTypeParameter = actualTypeArguments[0];
458         if (returnTypeParameter instanceof Class<?>) {
459           returnType = (Class<?>) returnTypeParameter;
460         }
461       }
462     }
463 
464     return returnType;
465   }
466 
467   private void applyResults(Result[] results, Class<?> resultType, List<ResultMapping> resultMappings) {
468     for (Result result : results) {
469       List<ResultFlag> flags = new ArrayList<>();
470       if (result.id()) {
471         flags.add(ResultFlag.ID);
472       }
473       @SuppressWarnings("unchecked")
474       Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>)
475               ((result.typeHandler() == UnknownTypeHandler.class) ? null : result.typeHandler());
476       boolean hasNestedResultMap = hasNestedResultMap(result);
477       ResultMapping resultMapping = assistant.buildResultMapping(
478           resultType,
479           nullOrEmpty(result.property()),
480           nullOrEmpty(result.column()),
481           result.javaType() == void.class ? null : result.javaType(),
482           result.jdbcType() == JdbcType.UNDEFINED ? null : result.jdbcType(),
483           hasNestedSelect(result) ? nestedSelectId(result) : null,
484           hasNestedResultMap ? nestedResultMapId(result) : null,
485           null,
486           hasNestedResultMap ? findColumnPrefix(result) : null,
487           typeHandler,
488           flags,
489           null,
490           null,
491           isLazy(result));
492       resultMappings.add(resultMapping);
493     }
494   }
495 
496   private String findColumnPrefix(Result result) {
497     String columnPrefix = result.one().columnPrefix();
498     if (columnPrefix.length() < 1) {
499       columnPrefix = result.many().columnPrefix();
500     }
501     return columnPrefix;
502   }
503 
504   private String nestedResultMapId(Result result) {
505     String resultMapId = result.one().resultMap();
506     if (resultMapId.length() < 1) {
507       resultMapId = result.many().resultMap();
508     }
509     if (!resultMapId.contains(".")) {
510       resultMapId = type.getName() + "." + resultMapId;
511     }
512     return resultMapId;
513   }
514 
515   private boolean hasNestedResultMap(Result result) {
516     if (result.one().resultMap().length() > 0 && result.many().resultMap().length() > 0) {
517       throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
518     }
519     return result.one().resultMap().length() > 0 || result.many().resultMap().length() > 0;
520   }
521 
522   private String nestedSelectId(Result result) {
523     String nestedSelect = result.one().select();
524     if (nestedSelect.length() < 1) {
525       nestedSelect = result.many().select();
526     }
527     if (!nestedSelect.contains(".")) {
528       nestedSelect = type.getName() + "." + nestedSelect;
529     }
530     return nestedSelect;
531   }
532 
533   private boolean isLazy(Result result) {
534     boolean isLazy = configuration.isLazyLoadingEnabled();
535     if (result.one().select().length() > 0 && FetchType.DEFAULT != result.one().fetchType()) {
536       isLazy = result.one().fetchType() == FetchType.LAZY;
537     } else if (result.many().select().length() > 0 && FetchType.DEFAULT != result.many().fetchType()) {
538       isLazy = result.many().fetchType() == FetchType.LAZY;
539     }
540     return isLazy;
541   }
542 
543   private boolean hasNestedSelect(Result result) {
544     if (result.one().select().length() > 0 && result.many().select().length() > 0) {
545       throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
546     }
547     return result.one().select().length() > 0 || result.many().select().length() > 0;
548   }
549 
550   private void applyConstructorArgs(Arg[] args, Class<?> resultType, List<ResultMapping> resultMappings) {
551     for (Arg arg : args) {
552       List<ResultFlag> flags = new ArrayList<>();
553       flags.add(ResultFlag.CONSTRUCTOR);
554       if (arg.id()) {
555         flags.add(ResultFlag.ID);
556       }
557       @SuppressWarnings("unchecked")
558       Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>)
559               (arg.typeHandler() == UnknownTypeHandler.class ? null : arg.typeHandler());
560       ResultMapping resultMapping = assistant.buildResultMapping(
561           resultType,
562           nullOrEmpty(arg.name()),
563           nullOrEmpty(arg.column()),
564           arg.javaType() == void.class ? null : arg.javaType(),
565           arg.jdbcType() == JdbcType.UNDEFINED ? null : arg.jdbcType(),
566           nullOrEmpty(arg.select()),
567           nullOrEmpty(arg.resultMap()),
568           null,
569           nullOrEmpty(arg.columnPrefix()),
570           typeHandler,
571           flags,
572           null,
573           null,
574           false);
575       resultMappings.add(resultMapping);
576     }
577   }
578 
579   private String nullOrEmpty(String value) {
580     return value == null || value.trim().length() == 0 ? null : value;
581   }
582 
583   private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
584     String id = baseStatementId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
585     Class<?> resultTypeClass = selectKeyAnnotation.resultType();
586     StatementType statementType = selectKeyAnnotation.statementType();
587     String keyProperty = selectKeyAnnotation.keyProperty();
588     String keyColumn = selectKeyAnnotation.keyColumn();
589     boolean executeBefore = selectKeyAnnotation.before();
590 
591     // defaults
592     boolean useCache = false;
593     KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
594     Integer fetchSize = null;
595     Integer timeout = null;
596     boolean flushCache = false;
597     String parameterMap = null;
598     String resultMap = null;
599     ResultSetType resultSetTypeEnum = null;
600     String databaseId = selectKeyAnnotation.databaseId().isEmpty() ? null : selectKeyAnnotation.databaseId();
601 
602     SqlSource sqlSource = buildSqlSource(selectKeyAnnotation, parameterTypeClass, languageDriver, null);
603     SqlCommandType sqlCommandType = SqlCommandType.SELECT;
604 
605     assistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum,
606         flushCache, useCache, false,
607         keyGenerator, keyProperty, keyColumn, databaseId, languageDriver, null);
608 
609     id = assistant.applyCurrentNamespace(id, false);
610 
611     MappedStatement keyStatement = configuration.getMappedStatement(id, false);
612     SelectKeyGenerator answer = new SelectKeyGenerator(keyStatement, executeBefore);
613     configuration.addKeyGenerator(id, answer);
614     return answer;
615   }
616 
617   private SqlSource buildSqlSource(Annotation annotation, Class<?> parameterType, LanguageDriver languageDriver,
618       Method method) {
619     if (annotation instanceof Select) {
620       return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, languageDriver);
621     } else if (annotation instanceof Update) {
622       return buildSqlSourceFromStrings(((Update) annotation).value(), parameterType, languageDriver);
623     } else if (annotation instanceof Insert) {
624       return buildSqlSourceFromStrings(((Insert) annotation).value(), parameterType, languageDriver);
625     } else if (annotation instanceof Delete) {
626       return buildSqlSourceFromStrings(((Delete) annotation).value(), parameterType, languageDriver);
627     } else if (annotation instanceof SelectKey) {
628       return buildSqlSourceFromStrings(((SelectKey) annotation).statement(), parameterType, languageDriver);
629     }
630     return new ProviderSqlSource(assistant.getConfiguration(), annotation, type, method);
631   }
632 
633   private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass,
634       LanguageDriver languageDriver) {
635     return languageDriver.createSqlSource(configuration, String.join(" ", strings).trim(), parameterTypeClass);
636   }
637 
638   @SafeVarargs
639   private final Optional<AnnotationWrapper> getAnnotationWrapper(Method method, boolean errorIfNoMatch,
640       Class<? extends Annotation>... targetTypes) {
641     return getAnnotationWrapper(method, errorIfNoMatch, Arrays.asList(targetTypes));
642   }
643 
644   private Optional<AnnotationWrapper> getAnnotationWrapper(Method method, boolean errorIfNoMatch,
645       Collection<Class<? extends Annotation>> targetTypes) {
646     String databaseId = configuration.getDatabaseId();
647     Map<String, AnnotationWrapper> statementAnnotations = targetTypes.stream()
648         .flatMap(x -> Arrays.stream(method.getAnnotationsByType(x))).map(AnnotationWrapper::new)
649         .collect(Collectors.toMap(AnnotationWrapper::getDatabaseId, x -> x, (existing, duplicate) -> {
650           throw new BuilderException(String.format("Detected conflicting annotations '%s' and '%s' on '%s'.",
651               existing.getAnnotation(), duplicate.getAnnotation(),
652               method.getDeclaringClass().getName() + "." + method.getName()));
653         }));
654     AnnotationWrapper annotationWrapper = null;
655     if (databaseId != null) {
656       annotationWrapper = statementAnnotations.get(databaseId);
657     }
658     if (annotationWrapper == null) {
659       annotationWrapper = statementAnnotations.get("");
660     }
661     if (errorIfNoMatch && annotationWrapper == null && !statementAnnotations.isEmpty()) {
662       // Annotations exist, but there is no matching one for the specified databaseId
663       throw new BuilderException(
664           String.format(
665               "Could not find a statement annotation that correspond a current database or default statement on method '%s.%s'. Current database id is [%s].",
666               method.getDeclaringClass().getName(), method.getName(), databaseId));
667     }
668     return Optional.ofNullable(annotationWrapper);
669   }
670 
671   private class AnnotationWrapper {
672     private final Annotation annotation;
673     private final String databaseId;
674     private final SqlCommandType sqlCommandType;
675 
676     AnnotationWrapper(Annotation annotation) {
677       super();
678       this.annotation = annotation;
679       if (annotation instanceof Select) {
680         databaseId = ((Select) annotation).databaseId();
681         sqlCommandType = SqlCommandType.SELECT;
682       } else if (annotation instanceof Update) {
683         databaseId = ((Update) annotation).databaseId();
684         sqlCommandType = SqlCommandType.UPDATE;
685       } else if (annotation instanceof Insert) {
686         databaseId = ((Insert) annotation).databaseId();
687         sqlCommandType = SqlCommandType.INSERT;
688       } else if (annotation instanceof Delete) {
689         databaseId = ((Delete) annotation).databaseId();
690         sqlCommandType = SqlCommandType.DELETE;
691       } else if (annotation instanceof SelectProvider) {
692         databaseId = ((SelectProvider) annotation).databaseId();
693         sqlCommandType = SqlCommandType.SELECT;
694       } else if (annotation instanceof UpdateProvider) {
695         databaseId = ((UpdateProvider) annotation).databaseId();
696         sqlCommandType = SqlCommandType.UPDATE;
697       } else if (annotation instanceof InsertProvider) {
698         databaseId = ((InsertProvider) annotation).databaseId();
699         sqlCommandType = SqlCommandType.INSERT;
700       } else if (annotation instanceof DeleteProvider) {
701         databaseId = ((DeleteProvider) annotation).databaseId();
702         sqlCommandType = SqlCommandType.DELETE;
703       } else {
704         sqlCommandType = SqlCommandType.UNKNOWN;
705         if (annotation instanceof Options) {
706           databaseId = ((Options) annotation).databaseId();
707         } else if (annotation instanceof SelectKey) {
708           databaseId = ((SelectKey) annotation).databaseId();
709         } else {
710           databaseId = "";
711         }
712       }
713     }
714 
715     Annotation getAnnotation() {
716       return annotation;
717     }
718 
719     SqlCommandType getSqlCommandType() {
720       return sqlCommandType;
721     }
722 
723     String getDatabaseId() {
724       return databaseId;
725     }
726   }
727 }