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