ProviderSqlSource.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.lang.annotation.Annotation;
  18. import java.lang.reflect.InvocationTargetException;
  19. import java.lang.reflect.Method;
  20. import java.lang.reflect.Modifier;
  21. import java.util.Map;

  22. import org.apache.ibatis.annotations.Lang;
  23. import org.apache.ibatis.builder.BuilderException;
  24. import org.apache.ibatis.mapping.BoundSql;
  25. import org.apache.ibatis.mapping.SqlSource;
  26. import org.apache.ibatis.reflection.ParamNameResolver;
  27. import org.apache.ibatis.scripting.LanguageDriver;
  28. import org.apache.ibatis.session.Configuration;

  29. /**
  30.  * @author Clinton Begin
  31.  * @author Kazuki Shimizu
  32.  */
  33. public class ProviderSqlSource implements SqlSource {

  34.   private final Configuration configuration;
  35.   private final Class<?> providerType;
  36.   private final LanguageDriver languageDriver;
  37.   private final Method mapperMethod;
  38.   private final Method providerMethod;
  39.   private final String[] providerMethodArgumentNames;
  40.   private final Class<?>[] providerMethodParameterTypes;
  41.   private final ProviderContext providerContext;
  42.   private final Integer providerContextIndex;

  43.   /**
  44.    * This constructor will remove at a future version.
  45.    *
  46.    * @param configuration
  47.    *          the configuration
  48.    * @param provider
  49.    *          the provider
  50.    *
  51.    * @deprecated Since 3.5.3, Please use the {@link #ProviderSqlSource(Configuration, Annotation, Class, Method)}
  52.    *             instead of this.
  53.    */
  54.   @Deprecated
  55.   public ProviderSqlSource(Configuration configuration, Object provider) {
  56.     this(configuration, provider, null, null);
  57.   }

  58.   /**
  59.    * This constructor will remove at a future version.
  60.    *
  61.    * @param configuration
  62.    *          the configuration
  63.    * @param provider
  64.    *          the provider
  65.    * @param mapperType
  66.    *          the mapper type
  67.    * @param mapperMethod
  68.    *          the mapper method
  69.    *
  70.    * @since 3.4.5
  71.    *
  72.    * @deprecated Since 3.5.3, Please use the {@link #ProviderSqlSource(Configuration, Annotation, Class, Method)}
  73.    *             instead of this.
  74.    */
  75.   @Deprecated
  76.   public ProviderSqlSource(Configuration configuration, Object provider, Class<?> mapperType, Method mapperMethod) {
  77.     this(configuration, (Annotation) provider, mapperType, mapperMethod);
  78.   }

  79.   /**
  80.    * Instantiates a new provider sql source.
  81.    *
  82.    * @param configuration
  83.    *          the configuration
  84.    * @param provider
  85.    *          the provider
  86.    * @param mapperType
  87.    *          the mapper type
  88.    * @param mapperMethod
  89.    *          the mapper method
  90.    *
  91.    * @since 3.5.3
  92.    */
  93.   public ProviderSqlSource(Configuration configuration, Annotation provider, Class<?> mapperType, Method mapperMethod) {
  94.     String candidateProviderMethodName;
  95.     Method candidateProviderMethod = null;
  96.     try {
  97.       this.configuration = configuration;
  98.       this.mapperMethod = mapperMethod;
  99.       Lang lang = mapperMethod == null ? null : mapperMethod.getAnnotation(Lang.class);
  100.       this.languageDriver = configuration.getLanguageDriver(lang == null ? null : lang.value());
  101.       this.providerType = getProviderType(configuration, provider, mapperMethod);
  102.       candidateProviderMethodName = (String) provider.annotationType().getMethod("method").invoke(provider);

  103.       if (candidateProviderMethodName.length() == 0
  104.           && ProviderMethodResolver.class.isAssignableFrom(this.providerType)) {
  105.         candidateProviderMethod = ((ProviderMethodResolver) this.providerType.getDeclaredConstructor().newInstance())
  106.             .resolveMethod(new ProviderContext(mapperType, mapperMethod, configuration.getDatabaseId()));
  107.       }
  108.       if (candidateProviderMethod == null) {
  109.         candidateProviderMethodName = candidateProviderMethodName.length() == 0 ? "provideSql"
  110.             : candidateProviderMethodName;
  111.         for (Method m : this.providerType.getMethods()) {
  112.           if (candidateProviderMethodName.equals(m.getName())
  113.               && CharSequence.class.isAssignableFrom(m.getReturnType())) {
  114.             if (candidateProviderMethod != null) {
  115.               throw new BuilderException("Error creating SqlSource for SqlProvider. Method '"
  116.                   + candidateProviderMethodName + "' is found multiple in SqlProvider '" + this.providerType.getName()
  117.                   + "'. Sql provider method can not overload.");
  118.             }
  119.             candidateProviderMethod = m;
  120.           }
  121.         }
  122.       }
  123.     } catch (BuilderException e) {
  124.       throw e;
  125.     } catch (Exception e) {
  126.       throw new BuilderException("Error creating SqlSource for SqlProvider.  Cause: " + e, e);
  127.     }
  128.     if (candidateProviderMethod == null) {
  129.       throw new BuilderException("Error creating SqlSource for SqlProvider. Method '" + candidateProviderMethodName
  130.           + "' not found in SqlProvider '" + this.providerType.getName() + "'.");
  131.     }
  132.     this.providerMethod = candidateProviderMethod;
  133.     this.providerMethodArgumentNames = new ParamNameResolver(configuration, this.providerMethod).getNames();
  134.     this.providerMethodParameterTypes = this.providerMethod.getParameterTypes();

  135.     ProviderContext candidateProviderContext = null;
  136.     Integer candidateProviderContextIndex = null;
  137.     for (int i = 0; i < this.providerMethodParameterTypes.length; i++) {
  138.       Class<?> parameterType = this.providerMethodParameterTypes[i];
  139.       if (parameterType == ProviderContext.class) {
  140.         if (candidateProviderContext != null) {
  141.           throw new BuilderException(
  142.               "Error creating SqlSource for SqlProvider. ProviderContext found multiple in SqlProvider method ("
  143.                   + this.providerType.getName() + "." + providerMethod.getName()
  144.                   + "). ProviderContext can not define multiple in SqlProvider method argument.");
  145.         }
  146.         candidateProviderContext = new ProviderContext(mapperType, mapperMethod, configuration.getDatabaseId());
  147.         candidateProviderContextIndex = i;
  148.       }
  149.     }
  150.     this.providerContext = candidateProviderContext;
  151.     this.providerContextIndex = candidateProviderContextIndex;
  152.   }

  153.   @Override
  154.   public BoundSql getBoundSql(Object parameterObject) {
  155.     SqlSource sqlSource = createSqlSource(parameterObject);
  156.     return sqlSource.getBoundSql(parameterObject);
  157.   }

  158.   private SqlSource createSqlSource(Object parameterObject) {
  159.     try {
  160.       String sql;
  161.       if (parameterObject instanceof Map) {
  162.         int bindParameterCount = providerMethodParameterTypes.length - (providerContext == null ? 0 : 1);
  163.         if (bindParameterCount == 1
  164.             && providerMethodParameterTypes[Integer.valueOf(0).equals(providerContextIndex) ? 1 : 0]
  165.                 .isAssignableFrom(parameterObject.getClass())) {
  166.           sql = invokeProviderMethod(extractProviderMethodArguments(parameterObject));
  167.         } else {
  168.           @SuppressWarnings("unchecked")
  169.           Map<String, Object> params = (Map<String, Object>) parameterObject;
  170.           sql = invokeProviderMethod(extractProviderMethodArguments(params, providerMethodArgumentNames));
  171.         }
  172.       } else {
  173.         switch (providerMethodParameterTypes.length) {
  174.           case 0:
  175.             sql = invokeProviderMethod();
  176.             break;
  177.           case 1:
  178.             if (providerContext == null) {
  179.               sql = invokeProviderMethod(parameterObject);
  180.             } else {
  181.               sql = invokeProviderMethod(providerContext);
  182.             }
  183.             break;
  184.           case 2:
  185.             sql = invokeProviderMethod(extractProviderMethodArguments(parameterObject));
  186.             break;
  187.           default:
  188.             throw new BuilderException("Cannot invoke SqlProvider method '" + providerMethod
  189.                 + "' with specify parameter '" + (parameterObject == null ? null : parameterObject.getClass())
  190.                 + "' because SqlProvider method arguments for '" + mapperMethod + "' is an invalid combination.");
  191.         }
  192.       }
  193.       Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
  194.       return languageDriver.createSqlSource(configuration, sql, parameterType);
  195.     } catch (BuilderException e) {
  196.       throw e;
  197.     } catch (Exception e) {
  198.       throw new BuilderException("Error invoking SqlProvider method '" + providerMethod + "' with specify parameter '"
  199.           + (parameterObject == null ? null : parameterObject.getClass()) + "'.  Cause: " + extractRootCause(e), e);
  200.     }
  201.   }

  202.   private Throwable extractRootCause(Exception e) {
  203.     Throwable cause = e;
  204.     while (cause.getCause() != null) {
  205.       cause = cause.getCause();
  206.     }
  207.     return cause;
  208.   }

  209.   private Object[] extractProviderMethodArguments(Object parameterObject) {
  210.     if (providerContext != null) {
  211.       Object[] args = new Object[2];
  212.       args[providerContextIndex == 0 ? 1 : 0] = parameterObject;
  213.       args[providerContextIndex] = providerContext;
  214.       return args;
  215.     }
  216.     return new Object[] { parameterObject };
  217.   }

  218.   private Object[] extractProviderMethodArguments(Map<String, Object> params, String[] argumentNames) {
  219.     Object[] args = new Object[argumentNames.length];
  220.     for (int i = 0; i < args.length; i++) {
  221.       if (providerContextIndex != null && providerContextIndex == i) {
  222.         args[i] = providerContext;
  223.       } else {
  224.         args[i] = params.get(argumentNames[i]);
  225.       }
  226.     }
  227.     return args;
  228.   }

  229.   private String invokeProviderMethod(Object... args) throws Exception {
  230.     Object targetObject = null;
  231.     if (!Modifier.isStatic(providerMethod.getModifiers())) {
  232.       targetObject = providerType.getDeclaredConstructor().newInstance();
  233.     }
  234.     CharSequence sql = (CharSequence) providerMethod.invoke(targetObject, args);
  235.     return sql != null ? sql.toString() : null;
  236.   }

  237.   private Class<?> getProviderType(Configuration configuration, Annotation providerAnnotation, Method mapperMethod)
  238.       throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
  239.     Class<?> type = (Class<?>) providerAnnotation.annotationType().getMethod("type").invoke(providerAnnotation);
  240.     Class<?> value = (Class<?>) providerAnnotation.annotationType().getMethod("value").invoke(providerAnnotation);
  241.     if (value == void.class && type == void.class) {
  242.       if (configuration.getDefaultSqlProviderType() != null) {
  243.         return configuration.getDefaultSqlProviderType();
  244.       }
  245.       throw new BuilderException("Please specify either 'value' or 'type' attribute of @"
  246.           + providerAnnotation.annotationType().getSimpleName() + " at the '" + mapperMethod.toString() + "'.");
  247.     }
  248.     if (value != void.class && type != void.class && value != type) {
  249.       throw new BuilderException("Cannot specify different class on 'value' and 'type' attribute of @"
  250.           + providerAnnotation.annotationType().getSimpleName() + " at the '" + mapperMethod.toString() + "'.");
  251.     }
  252.     return value == void.class ? type : value;
  253.   }

  254. }