MapperAnnotationBuilder.java

  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. import java.io.IOException;
  18. import java.io.InputStream;
  19. import java.lang.annotation.Annotation;
  20. import java.lang.reflect.Array;
  21. import java.lang.reflect.GenericArrayType;
  22. import java.lang.reflect.Method;
  23. import java.lang.reflect.ParameterizedType;
  24. import java.lang.reflect.Type;
  25. import java.util.ArrayList;
  26. import java.util.Arrays;
  27. import java.util.Collection;
  28. import java.util.HashMap;
  29. import java.util.List;
  30. import java.util.Map;
  31. import java.util.Optional;
  32. import java.util.Properties;
  33. import java.util.Set;
  34. import java.util.stream.Collectors;
  35. import java.util.stream.Stream;

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

  89. /**
  90.  * @author Clinton Begin
  91.  * @author Kazuki Shimizu
  92.  */
  93. public class MapperAnnotationBuilder {

  94.   private static final Set<Class<? extends Annotation>> statementAnnotationTypes = Stream
  95.       .of(Select.class, Update.class, Insert.class, Delete.class, SelectProvider.class, UpdateProvider.class,
  96.           InsertProvider.class, DeleteProvider.class)
  97.       .collect(Collectors.toSet());

  98.   private final Configuration configuration;
  99.   private final MapperBuilderAssistant assistant;
  100.   private final Class<?> type;

  101.   public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
  102.     String resource = type.getName().replace('.', '/') + ".java (best guess)";
  103.     this.assistant = new MapperBuilderAssistant(configuration, resource);
  104.     this.configuration = configuration;
  105.     this.type = type;
  106.   }

  107.   public void parse() {
  108.     String resource = type.toString();
  109.     if (!configuration.isResourceLoaded(resource)) {
  110.       loadXmlResource();
  111.       configuration.addLoadedResource(resource);
  112.       assistant.setCurrentNamespace(type.getName());
  113.       parseCache();
  114.       parseCacheRef();
  115.       for (Method method : type.getMethods()) {
  116.         if (!canHaveStatement(method)) {
  117.           continue;
  118.         }
  119.         if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
  120.             && method.getAnnotation(ResultMap.class) == null) {
  121.           parseResultMap(method);
  122.         }
  123.         try {
  124.           parseStatement(method);
  125.         } catch (IncompleteElementException e) {
  126.           configuration.addIncompleteMethod(new MethodResolver(this, method));
  127.         }
  128.       }
  129.     }
  130.     configuration.parsePendingMethods(false);
  131.   }

  132.   private static boolean canHaveStatement(Method method) {
  133.     // issue #237
  134.     return !method.isBridge() && !method.isDefault();
  135.   }

  136.   private void loadXmlResource() {
  137.     // Spring may not know the real resource name so we check a flag
  138.     // to prevent loading again a resource twice
  139.     // this flag is set at XMLMapperBuilder#bindMapperForNamespace
  140.     if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
  141.       String xmlResource = type.getName().replace('.', '/') + ".xml";
  142.       // #1347
  143.       InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
  144.       if (inputStream == null) {
  145.         // Search XML mapper that is not in the module but in the classpath.
  146.         try {
  147.           inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
  148.         } catch (IOException e2) {
  149.           // ignore, resource is not required
  150.         }
  151.       }
  152.       if (inputStream != null) {
  153.         XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource,
  154.             configuration.getSqlFragments(), type.getName());
  155.         xmlParser.parse();
  156.       }
  157.     }
  158.   }

  159.   private void parseCache() {
  160.     CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
  161.     if (cacheDomain != null) {
  162.       Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
  163.       Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
  164.       Properties props = convertToProperties(cacheDomain.properties());
  165.       assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size,
  166.           cacheDomain.readWrite(), cacheDomain.blocking(), props);
  167.     }
  168.   }

  169.   private Properties convertToProperties(Property[] properties) {
  170.     if (properties.length == 0) {
  171.       return null;
  172.     }
  173.     Properties props = new Properties();
  174.     for (Property property : properties) {
  175.       props.setProperty(property.name(), PropertyParser.parse(property.value(), configuration.getVariables()));
  176.     }
  177.     return props;
  178.   }

  179.   private void parseCacheRef() {
  180.     CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
  181.     if (cacheDomainRef != null) {
  182.       Class<?> refType = cacheDomainRef.value();
  183.       String refName = cacheDomainRef.name();
  184.       if (refType == void.class && refName.isEmpty()) {
  185.         throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
  186.       }
  187.       if (refType != void.class && !refName.isEmpty()) {
  188.         throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
  189.       }
  190.       String namespace = refType != void.class ? refType.getName() : refName;
  191.       try {
  192.         assistant.useCacheRef(namespace);
  193.       } catch (IncompleteElementException e) {
  194.         configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
  195.       }
  196.     }
  197.   }

  198.   private String parseResultMap(Method method) {
  199.     Class<?> returnType = getReturnType(method, type);
  200.     Arg[] args = method.getAnnotationsByType(Arg.class);
  201.     Result[] results = method.getAnnotationsByType(Result.class);
  202.     TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
  203.     String resultMapId = generateResultMapName(method);
  204.     applyResultMap(resultMapId, returnType, args, results, typeDiscriminator);
  205.     return resultMapId;
  206.   }

  207.   private String generateResultMapName(Method method) {
  208.     Results results = method.getAnnotation(Results.class);
  209.     if (results != null && !results.id().isEmpty()) {
  210.       return type.getName() + "." + results.id();
  211.     }
  212.     StringBuilder suffix = new StringBuilder();
  213.     for (Class<?> c : method.getParameterTypes()) {
  214.       suffix.append("-");
  215.       suffix.append(c.getSimpleName());
  216.     }
  217.     if (suffix.length() < 1) {
  218.       suffix.append("-void");
  219.     }
  220.     return type.getName() + "." + method.getName() + suffix;
  221.   }

  222.   private void applyResultMap(String resultMapId, Class<?> returnType, Arg[] args, Result[] results,
  223.       TypeDiscriminator discriminator) {
  224.     List<ResultMapping> resultMappings = new ArrayList<>();
  225.     applyConstructorArgs(args, returnType, resultMappings);
  226.     applyResults(results, returnType, resultMappings);
  227.     Discriminator disc = applyDiscriminator(resultMapId, returnType, discriminator);
  228.     // TODO add AutoMappingBehaviour
  229.     assistant.addResultMap(resultMapId, returnType, null, disc, resultMappings, null);
  230.     createDiscriminatorResultMaps(resultMapId, returnType, discriminator);
  231.   }

  232.   private void createDiscriminatorResultMaps(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
  233.     if (discriminator != null) {
  234.       for (Case c : discriminator.cases()) {
  235.         String caseResultMapId = resultMapId + "-" + c.value();
  236.         List<ResultMapping> resultMappings = new ArrayList<>();
  237.         // issue #136
  238.         applyConstructorArgs(c.constructArgs(), resultType, resultMappings);
  239.         applyResults(c.results(), resultType, resultMappings);
  240.         // TODO add AutoMappingBehaviour
  241.         assistant.addResultMap(caseResultMapId, c.type(), resultMapId, null, resultMappings, null);
  242.       }
  243.     }
  244.   }

  245.   private Discriminator applyDiscriminator(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
  246.     if (discriminator != null) {
  247.       String column = discriminator.column();
  248.       Class<?> javaType = discriminator.javaType() == void.class ? String.class : discriminator.javaType();
  249.       JdbcType jdbcType = discriminator.jdbcType() == JdbcType.UNDEFINED ? null : discriminator.jdbcType();
  250.       @SuppressWarnings("unchecked")
  251.       Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>) (discriminator
  252.           .typeHandler() == UnknownTypeHandler.class ? null : discriminator.typeHandler());
  253.       Case[] cases = discriminator.cases();
  254.       Map<String, String> discriminatorMap = new HashMap<>();
  255.       for (Case c : cases) {
  256.         String value = c.value();
  257.         String caseResultMapId = resultMapId + "-" + value;
  258.         discriminatorMap.put(value, caseResultMapId);
  259.       }
  260.       return assistant.buildDiscriminator(resultType, column, javaType, jdbcType, typeHandler, discriminatorMap);
  261.     }
  262.     return null;
  263.   }

  264.   void parseStatement(Method method) {
  265.     final Class<?> parameterTypeClass = getParameterType(method);
  266.     final LanguageDriver languageDriver = getLanguageDriver(method);

  267.     getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> {
  268.       final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass,
  269.           languageDriver, method);
  270.       final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType();
  271.       final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options) x.getAnnotation())
  272.           .orElse(null);
  273.       final String mappedStatementId = type.getName() + "." + method.getName();

  274.       final KeyGenerator keyGenerator;
  275.       String keyProperty = null;
  276.       String keyColumn = null;
  277.       if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
  278.         // first check for SelectKey annotation - that overrides everything else
  279.         SelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class)
  280.             .map(x -> (SelectKey) x.getAnnotation()).orElse(null);
  281.         if (selectKey != null) {
  282.           keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method),
  283.               languageDriver);
  284.           keyProperty = selectKey.keyProperty();
  285.         } else if (options == null) {
  286.           keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  287.         } else {
  288.           keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  289.           keyProperty = options.keyProperty();
  290.           keyColumn = options.keyColumn();
  291.         }
  292.       } else {
  293.         keyGenerator = NoKeyGenerator.INSTANCE;
  294.       }

  295.       Integer fetchSize = null;
  296.       Integer timeout = null;
  297.       StatementType statementType = StatementType.PREPARED;
  298.       ResultSetType resultSetType = configuration.getDefaultResultSetType();
  299.       boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  300.       boolean flushCache = !isSelect;
  301.       boolean useCache = isSelect;
  302.       if (options != null) {
  303.         if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
  304.           flushCache = true;
  305.         } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
  306.           flushCache = false;
  307.         }
  308.         useCache = options.useCache();
  309.         // issue #348
  310.         fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null;
  311.         timeout = options.timeout() > -1 ? options.timeout() : null;
  312.         statementType = options.statementType();
  313.         if (options.resultSetType() != ResultSetType.DEFAULT) {
  314.           resultSetType = options.resultSetType();
  315.         }
  316.       }

  317.       String resultMapId = null;
  318.       if (isSelect) {
  319.         ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
  320.         if (resultMapAnnotation != null) {
  321.           resultMapId = String.join(",", resultMapAnnotation.value());
  322.         } else {
  323.           resultMapId = generateResultMapName(method);
  324.         }
  325.       }

  326.       assistant.addMappedStatement(mappedStatementId, sqlSource, statementType, sqlCommandType, fetchSize, timeout,
  327.           // ParameterMapID
  328.           null, parameterTypeClass, resultMapId, getReturnType(method, type), resultSetType, flushCache, useCache,
  329.           // TODO gcode issue #577
  330.           false, keyGenerator, keyProperty, keyColumn, statementAnnotation.getDatabaseId(), languageDriver,
  331.           // ResultSets
  332.           options != null ? nullOrEmpty(options.resultSets()) : null, statementAnnotation.isDirtySelect());
  333.     });
  334.   }

  335.   private LanguageDriver getLanguageDriver(Method method) {
  336.     Lang lang = method.getAnnotation(Lang.class);
  337.     Class<? extends LanguageDriver> langClass = null;
  338.     if (lang != null) {
  339.       langClass = lang.value();
  340.     }
  341.     return configuration.getLanguageDriver(langClass);
  342.   }

  343.   private Class<?> getParameterType(Method method) {
  344.     Class<?> parameterType = null;
  345.     Class<?>[] parameterTypes = method.getParameterTypes();
  346.     for (Class<?> currentParameterType : parameterTypes) {
  347.       if (!RowBounds.class.isAssignableFrom(currentParameterType)
  348.           && !ResultHandler.class.isAssignableFrom(currentParameterType)) {
  349.         if (parameterType == null) {
  350.           parameterType = currentParameterType;
  351.         } else {
  352.           // issue #135
  353.           parameterType = ParamMap.class;
  354.         }
  355.       }
  356.     }
  357.     return parameterType;
  358.   }

  359.   private static Class<?> getReturnType(Method method, Class<?> type) {
  360.     Class<?> returnType = method.getReturnType();
  361.     Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, type);
  362.     if (resolvedReturnType instanceof Class) {
  363.       returnType = (Class<?>) resolvedReturnType;
  364.       if (returnType.isArray()) {
  365.         returnType = returnType.getComponentType();
  366.       }
  367.       // gcode issue #508
  368.       if (void.class.equals(returnType)) {
  369.         ResultType rt = method.getAnnotation(ResultType.class);
  370.         if (rt != null) {
  371.           returnType = rt.value();
  372.         }
  373.       }
  374.     } else if (resolvedReturnType instanceof ParameterizedType) {
  375.       ParameterizedType parameterizedType = (ParameterizedType) resolvedReturnType;
  376.       Class<?> rawType = (Class<?>) parameterizedType.getRawType();
  377.       if (Collection.class.isAssignableFrom(rawType) || Cursor.class.isAssignableFrom(rawType)) {
  378.         Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
  379.         if (actualTypeArguments != null && actualTypeArguments.length == 1) {
  380.           Type returnTypeParameter = actualTypeArguments[0];
  381.           if (returnTypeParameter instanceof Class<?>) {
  382.             returnType = (Class<?>) returnTypeParameter;
  383.           } else if (returnTypeParameter instanceof ParameterizedType) {
  384.             // (gcode issue #443) actual type can be a also a parameterized type
  385.             returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
  386.           } else if (returnTypeParameter instanceof GenericArrayType) {
  387.             Class<?> componentType = (Class<?>) ((GenericArrayType) returnTypeParameter).getGenericComponentType();
  388.             // (gcode issue #525) support List<byte[]>
  389.             returnType = Array.newInstance(componentType, 0).getClass();
  390.           }
  391.         }
  392.       } else if (method.isAnnotationPresent(MapKey.class) && Map.class.isAssignableFrom(rawType)) {
  393.         // (gcode issue 504) Do not look into Maps if there is not MapKey annotation
  394.         Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
  395.         if (actualTypeArguments != null && actualTypeArguments.length == 2) {
  396.           Type returnTypeParameter = actualTypeArguments[1];
  397.           if (returnTypeParameter instanceof Class<?>) {
  398.             returnType = (Class<?>) returnTypeParameter;
  399.           } else if (returnTypeParameter instanceof ParameterizedType) {
  400.             // (gcode issue 443) actual type can be a also a parameterized type
  401.             returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
  402.           }
  403.         }
  404.       } else if (Optional.class.equals(rawType)) {
  405.         Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
  406.         Type returnTypeParameter = actualTypeArguments[0];
  407.         if (returnTypeParameter instanceof Class<?>) {
  408.           returnType = (Class<?>) returnTypeParameter;
  409.         }
  410.       }
  411.     }

  412.     return returnType;
  413.   }

  414.   private void applyResults(Result[] results, Class<?> resultType, List<ResultMapping> resultMappings) {
  415.     for (Result result : results) {
  416.       List<ResultFlag> flags = new ArrayList<>();
  417.       if (result.id()) {
  418.         flags.add(ResultFlag.ID);
  419.       }
  420.       @SuppressWarnings("unchecked")
  421.       Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>) (result
  422.           .typeHandler() == UnknownTypeHandler.class ? null : result.typeHandler());
  423.       boolean hasNestedResultMap = hasNestedResultMap(result);
  424.       ResultMapping resultMapping = assistant.buildResultMapping(resultType, nullOrEmpty(result.property()),
  425.           nullOrEmpty(result.column()), result.javaType() == void.class ? null : result.javaType(),
  426.           result.jdbcType() == JdbcType.UNDEFINED ? null : result.jdbcType(),
  427.           hasNestedSelect(result) ? nestedSelectId(result) : null,
  428.           hasNestedResultMap ? nestedResultMapId(result) : null, null,
  429.           hasNestedResultMap ? findColumnPrefix(result) : null, typeHandler, flags, null, null, isLazy(result));
  430.       resultMappings.add(resultMapping);
  431.     }
  432.   }

  433.   private String findColumnPrefix(Result result) {
  434.     String columnPrefix = result.one().columnPrefix();
  435.     if (columnPrefix.length() < 1) {
  436.       columnPrefix = result.many().columnPrefix();
  437.     }
  438.     return columnPrefix;
  439.   }

  440.   private String nestedResultMapId(Result result) {
  441.     String resultMapId = result.one().resultMap();
  442.     if (resultMapId.length() < 1) {
  443.       resultMapId = result.many().resultMap();
  444.     }
  445.     if (!resultMapId.contains(".")) {
  446.       resultMapId = type.getName() + "." + resultMapId;
  447.     }
  448.     return resultMapId;
  449.   }

  450.   private boolean hasNestedResultMap(Result result) {
  451.     if (result.one().resultMap().length() > 0 && result.many().resultMap().length() > 0) {
  452.       throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
  453.     }
  454.     return result.one().resultMap().length() > 0 || result.many().resultMap().length() > 0;
  455.   }

  456.   private String nestedSelectId(Result result) {
  457.     String nestedSelect = result.one().select();
  458.     if (nestedSelect.length() < 1) {
  459.       nestedSelect = result.many().select();
  460.     }
  461.     if (!nestedSelect.contains(".")) {
  462.       nestedSelect = type.getName() + "." + nestedSelect;
  463.     }
  464.     return nestedSelect;
  465.   }

  466.   private boolean isLazy(Result result) {
  467.     boolean isLazy = configuration.isLazyLoadingEnabled();
  468.     if (result.one().select().length() > 0 && FetchType.DEFAULT != result.one().fetchType()) {
  469.       isLazy = result.one().fetchType() == FetchType.LAZY;
  470.     } else if (result.many().select().length() > 0 && FetchType.DEFAULT != result.many().fetchType()) {
  471.       isLazy = result.many().fetchType() == FetchType.LAZY;
  472.     }
  473.     return isLazy;
  474.   }

  475.   private boolean hasNestedSelect(Result result) {
  476.     if (result.one().select().length() > 0 && result.many().select().length() > 0) {
  477.       throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
  478.     }
  479.     return result.one().select().length() > 0 || result.many().select().length() > 0;
  480.   }

  481.   private void applyConstructorArgs(Arg[] args, Class<?> resultType, List<ResultMapping> resultMappings) {
  482.     for (Arg arg : args) {
  483.       List<ResultFlag> flags = new ArrayList<>();
  484.       flags.add(ResultFlag.CONSTRUCTOR);
  485.       if (arg.id()) {
  486.         flags.add(ResultFlag.ID);
  487.       }
  488.       @SuppressWarnings("unchecked")
  489.       Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>) (arg
  490.           .typeHandler() == UnknownTypeHandler.class ? null : arg.typeHandler());
  491.       ResultMapping resultMapping = assistant.buildResultMapping(resultType, nullOrEmpty(arg.name()),
  492.           nullOrEmpty(arg.column()), arg.javaType() == void.class ? null : arg.javaType(),
  493.           arg.jdbcType() == JdbcType.UNDEFINED ? null : arg.jdbcType(), nullOrEmpty(arg.select()),
  494.           nullOrEmpty(arg.resultMap()), null, nullOrEmpty(arg.columnPrefix()), typeHandler, flags, null, null, false);
  495.       resultMappings.add(resultMapping);
  496.     }
  497.   }

  498.   private String nullOrEmpty(String value) {
  499.     return value == null || value.trim().length() == 0 ? null : value;
  500.   }

  501.   private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId,
  502.       Class<?> parameterTypeClass, LanguageDriver languageDriver) {
  503.     String id = baseStatementId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
  504.     Class<?> resultTypeClass = selectKeyAnnotation.resultType();
  505.     StatementType statementType = selectKeyAnnotation.statementType();
  506.     String keyProperty = selectKeyAnnotation.keyProperty();
  507.     String keyColumn = selectKeyAnnotation.keyColumn();
  508.     boolean executeBefore = selectKeyAnnotation.before();

  509.     // defaults
  510.     boolean useCache = false;
  511.     KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
  512.     Integer fetchSize = null;
  513.     Integer timeout = null;
  514.     boolean flushCache = false;
  515.     String parameterMap = null;
  516.     String resultMap = null;
  517.     ResultSetType resultSetTypeEnum = null;
  518.     String databaseId = selectKeyAnnotation.databaseId().isEmpty() ? null : selectKeyAnnotation.databaseId();

  519.     SqlSource sqlSource = buildSqlSource(selectKeyAnnotation, parameterTypeClass, languageDriver, null);
  520.     SqlCommandType sqlCommandType = SqlCommandType.SELECT;

  521.     assistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
  522.         parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, false, keyGenerator,
  523.         keyProperty, keyColumn, databaseId, languageDriver, null, false);

  524.     id = assistant.applyCurrentNamespace(id, false);

  525.     MappedStatement keyStatement = configuration.getMappedStatement(id, false);
  526.     SelectKeyGenerator answer = new SelectKeyGenerator(keyStatement, executeBefore);
  527.     configuration.addKeyGenerator(id, answer);
  528.     return answer;
  529.   }

  530.   private SqlSource buildSqlSource(Annotation annotation, Class<?> parameterType, LanguageDriver languageDriver,
  531.       Method method) {
  532.     if (annotation instanceof Select) {
  533.       return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, languageDriver);
  534.     }
  535.     if (annotation instanceof Update) {
  536.       return buildSqlSourceFromStrings(((Update) annotation).value(), parameterType, languageDriver);
  537.     } else if (annotation instanceof Insert) {
  538.       return buildSqlSourceFromStrings(((Insert) annotation).value(), parameterType, languageDriver);
  539.     } else if (annotation instanceof Delete) {
  540.       return buildSqlSourceFromStrings(((Delete) annotation).value(), parameterType, languageDriver);
  541.     } else if (annotation instanceof SelectKey) {
  542.       return buildSqlSourceFromStrings(((SelectKey) annotation).statement(), parameterType, languageDriver);
  543.     }
  544.     return new ProviderSqlSource(assistant.getConfiguration(), annotation, type, method);
  545.   }

  546.   private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass,
  547.       LanguageDriver languageDriver) {
  548.     return languageDriver.createSqlSource(configuration, String.join(" ", strings).trim(), parameterTypeClass);
  549.   }

  550.   @SafeVarargs
  551.   private final Optional<AnnotationWrapper> getAnnotationWrapper(Method method, boolean errorIfNoMatch,
  552.       Class<? extends Annotation>... targetTypes) {
  553.     return getAnnotationWrapper(method, errorIfNoMatch, Arrays.asList(targetTypes));
  554.   }

  555.   private Optional<AnnotationWrapper> getAnnotationWrapper(Method method, boolean errorIfNoMatch,
  556.       Collection<Class<? extends Annotation>> targetTypes) {
  557.     String databaseId = configuration.getDatabaseId();
  558.     Map<String, AnnotationWrapper> statementAnnotations = targetTypes.stream()
  559.         .flatMap(x -> Arrays.stream(method.getAnnotationsByType(x))).map(AnnotationWrapper::new)
  560.         .collect(Collectors.toMap(AnnotationWrapper::getDatabaseId, x -> x, (existing, duplicate) -> {
  561.           throw new BuilderException(
  562.               String.format("Detected conflicting annotations '%s' and '%s' on '%s'.", existing.getAnnotation(),
  563.                   duplicate.getAnnotation(), method.getDeclaringClass().getName() + "." + method.getName()));
  564.         }));
  565.     AnnotationWrapper annotationWrapper = null;
  566.     if (databaseId != null) {
  567.       annotationWrapper = statementAnnotations.get(databaseId);
  568.     }
  569.     if (annotationWrapper == null) {
  570.       annotationWrapper = statementAnnotations.get("");
  571.     }
  572.     if (errorIfNoMatch && annotationWrapper == null && !statementAnnotations.isEmpty()) {
  573.       // Annotations exist, but there is no matching one for the specified databaseId
  574.       throw new BuilderException(String.format(
  575.           "Could not find a statement annotation that correspond a current database or default statement on method '%s.%s'. Current database id is [%s].",
  576.           method.getDeclaringClass().getName(), method.getName(), databaseId));
  577.     }
  578.     return Optional.ofNullable(annotationWrapper);
  579.   }

  580.   public static Class<?> getMethodReturnType(String mapperFqn, String localStatementId) {
  581.     if (mapperFqn == null || localStatementId == null) {
  582.       return null;
  583.     }
  584.     try {
  585.       Class<?> mapperClass = Resources.classForName(mapperFqn);
  586.       for (Method method : mapperClass.getMethods()) {
  587.         if (method.getName().equals(localStatementId) && canHaveStatement(method)) {
  588.           return getReturnType(method, mapperClass);
  589.         }
  590.       }
  591.     } catch (ClassNotFoundException e) {
  592.       // No corresponding mapper interface which is OK
  593.     }
  594.     return null;
  595.   }

  596.   private static class AnnotationWrapper {
  597.     private final Annotation annotation;
  598.     private final String databaseId;
  599.     private final SqlCommandType sqlCommandType;
  600.     private boolean dirtySelect;

  601.     AnnotationWrapper(Annotation annotation) {
  602.       this.annotation = annotation;
  603.       if (annotation instanceof Select) {
  604.         databaseId = ((Select) annotation).databaseId();
  605.         sqlCommandType = SqlCommandType.SELECT;
  606.         dirtySelect = ((Select) annotation).affectData();
  607.       } else if (annotation instanceof Update) {
  608.         databaseId = ((Update) annotation).databaseId();
  609.         sqlCommandType = SqlCommandType.UPDATE;
  610.       } else if (annotation instanceof Insert) {
  611.         databaseId = ((Insert) annotation).databaseId();
  612.         sqlCommandType = SqlCommandType.INSERT;
  613.       } else if (annotation instanceof Delete) {
  614.         databaseId = ((Delete) annotation).databaseId();
  615.         sqlCommandType = SqlCommandType.DELETE;
  616.       } else if (annotation instanceof SelectProvider) {
  617.         databaseId = ((SelectProvider) annotation).databaseId();
  618.         sqlCommandType = SqlCommandType.SELECT;
  619.         dirtySelect = ((SelectProvider) annotation).affectData();
  620.       } else if (annotation instanceof UpdateProvider) {
  621.         databaseId = ((UpdateProvider) annotation).databaseId();
  622.         sqlCommandType = SqlCommandType.UPDATE;
  623.       } else if (annotation instanceof InsertProvider) {
  624.         databaseId = ((InsertProvider) annotation).databaseId();
  625.         sqlCommandType = SqlCommandType.INSERT;
  626.       } else if (annotation instanceof DeleteProvider) {
  627.         databaseId = ((DeleteProvider) annotation).databaseId();
  628.         sqlCommandType = SqlCommandType.DELETE;
  629.       } else {
  630.         sqlCommandType = SqlCommandType.UNKNOWN;
  631.         if (annotation instanceof Options) {
  632.           databaseId = ((Options) annotation).databaseId();
  633.         } else if (annotation instanceof SelectKey) {
  634.           databaseId = ((SelectKey) annotation).databaseId();
  635.         } else {
  636.           databaseId = "";
  637.         }
  638.       }
  639.     }

  640.     Annotation getAnnotation() {
  641.       return annotation;
  642.     }

  643.     SqlCommandType getSqlCommandType() {
  644.       return sqlCommandType;
  645.     }

  646.     String getDatabaseId() {
  647.       return databaseId;
  648.     }

  649.     boolean isDirtySelect() {
  650.       return dirtySelect;
  651.     }
  652.   }
  653. }