ThymeleafSqlSource.java

  1. /*
  2.  *    Copyright 2018-2022 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.mybatis.scripting.thymeleaf;

  17. import java.lang.reflect.InvocationTargetException;
  18. import java.util.Arrays;
  19. import java.util.Collection;
  20. import java.util.HashSet;
  21. import java.util.Locale;
  22. import java.util.Map;
  23. import java.util.Optional;
  24. import java.util.Properties;
  25. import java.util.Set;
  26. import java.util.function.BiFunction;

  27. import org.apache.ibatis.builder.SqlSourceBuilder;
  28. import org.apache.ibatis.mapping.BoundSql;
  29. import org.apache.ibatis.mapping.SqlSource;
  30. import org.apache.ibatis.reflection.MetaClass;
  31. import org.apache.ibatis.scripting.xmltags.DynamicContext;
  32. import org.apache.ibatis.session.Configuration;
  33. import org.thymeleaf.context.IContext;

  34. /**
  35.  * The {@code SqlSource} for integrating with Thymeleaf.
  36.  *
  37.  * @author Kazuki Shimizu
  38.  *
  39.  * @version 1.0.0
  40.  *
  41.  * @see ThymeleafLanguageDriver
  42.  */
  43. class ThymeleafSqlSource implements SqlSource {

  44.   private static class TemporaryTakeoverKeys {
  45.     private static final String CONFIGURATION = "__configuration__";
  46.     private static final String DYNAMIC_CONTEXT = "__dynamicContext__";
  47.     private static final String PROCESSING_PARAMETER_TYPE = "__processingParameterType__";
  48.   }

  49.   private final Configuration configuration;
  50.   private final SqlGenerator sqlGenerator;
  51.   private final SqlSourceBuilder sqlSourceBuilder;
  52.   private final String sqlTemplate;
  53.   private final Class<?> parameterType;

  54.   /**
  55.    * Constructor for for integrating with template engine provide by Thymeleaf.
  56.    *
  57.    * @param configuration
  58.    *          A configuration instance of MyBatis
  59.    * @param sqlGenerator
  60.    *          A sql generator using the Thymeleaf feature
  61.    * @param sqlTemplate
  62.    *          A template string of SQL (inline SQL or template file path)
  63.    * @param parameterType
  64.    *          A parameter type that specified at mapper method argument or xml element
  65.    */
  66.   ThymeleafSqlSource(Configuration configuration, SqlGenerator sqlGenerator, String sqlTemplate,
  67.       Class<?> parameterType) {
  68.     this.configuration = configuration;
  69.     this.sqlGenerator = sqlGenerator;
  70.     this.sqlTemplate = sqlTemplate;
  71.     this.parameterType = parameterType;
  72.     this.sqlSourceBuilder = new SqlSourceBuilder(configuration);
  73.   }

  74.   /**
  75.    * {@inheritDoc}
  76.    */
  77.   @Override
  78.   public BoundSql getBoundSql(Object parameterObject) {
  79.     Class<?> processingParameterType;
  80.     if (parameterType == null) {
  81.       processingParameterType = parameterObject == null ? Object.class : parameterObject.getClass();
  82.     } else {
  83.       processingParameterType = parameterType;
  84.     }

  85.     DynamicContext dynamicContext = new DynamicContext(configuration, parameterObject);
  86.     Map<String, Object> customVariables = dynamicContext.getBindings();
  87.     customVariables.put(TemporaryTakeoverKeys.CONFIGURATION, configuration);
  88.     customVariables.put(TemporaryTakeoverKeys.DYNAMIC_CONTEXT, dynamicContext);
  89.     customVariables.put(TemporaryTakeoverKeys.PROCESSING_PARAMETER_TYPE, processingParameterType);
  90.     String sql = sqlGenerator.generate(sqlTemplate, parameterObject, dynamicContext::bind, customVariables);

  91.     SqlSource sqlSource = sqlSourceBuilder.parse(sql, processingParameterType, dynamicContext.getBindings());
  92.     BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  93.     dynamicContext.getBindings().forEach(boundSql::setAdditionalParameter);

  94.     return boundSql;
  95.   }

  96.   /**
  97.    * The factory class for Thymeleaf's context.
  98.    *
  99.    * @since 1.0.2
  100.    */
  101.   static class ContextFactory implements BiFunction<Object, Map<String, Object>, IContext> {
  102.     /**
  103.      * {@inheritDoc}
  104.      */
  105.     @Override
  106.     public IContext apply(Object parameter, Map<String, Object> customVariable) {
  107.       Configuration configuration = (Configuration) customVariable.remove(TemporaryTakeoverKeys.CONFIGURATION);
  108.       DynamicContext dynamicContext = (DynamicContext) customVariable.remove(TemporaryTakeoverKeys.DYNAMIC_CONTEXT);
  109.       Class<?> processingParameterType = (Class<?>) customVariable
  110.           .remove(TemporaryTakeoverKeys.PROCESSING_PARAMETER_TYPE);
  111.       MyBatisBindingContext bindingContext = new MyBatisBindingContext(
  112.           parameter != null && configuration.getTypeHandlerRegistry().hasTypeHandler(processingParameterType));
  113.       dynamicContext.bind(MyBatisBindingContext.CONTEXT_VARIABLE_NAME, bindingContext);
  114.       IContext context;
  115.       if (parameter instanceof Map) {
  116.         @SuppressWarnings(value = "unchecked")
  117.         Map<String, Object> map = (Map<String, Object>) parameter;
  118.         context = new MapBasedContext(map, dynamicContext, configuration.getVariables());
  119.       } else {
  120.         MetaClass metaClass = MetaClass.forClass(processingParameterType, configuration.getReflectorFactory());
  121.         context = new MetaClassBasedContext(parameter, metaClass, processingParameterType, dynamicContext,
  122.             configuration.getVariables());
  123.       }
  124.       return context;
  125.     }
  126.   }

  127.   private abstract static class AbstractContext implements IContext {

  128.     private final DynamicContext dynamicContext;
  129.     private final Properties configurationProperties;
  130.     private final Set<String> variableNames;

  131.     private AbstractContext(DynamicContext dynamicContext, Properties configurationProperties) {
  132.       this.dynamicContext = dynamicContext;
  133.       this.configurationProperties = configurationProperties;
  134.       this.variableNames = new HashSet<>();
  135.       addVariableNames(dynamicContext.getBindings().keySet());
  136.       Optional.ofNullable(configurationProperties).ifPresent(v -> addVariableNames(v.stringPropertyNames()));
  137.     }

  138.     void addVariableNames(Collection<String> names) {
  139.       variableNames.addAll(names);
  140.     }

  141.     /**
  142.      * {@inheritDoc}
  143.      */
  144.     @Override
  145.     public Locale getLocale() {
  146.       return Locale.getDefault();
  147.     }

  148.     /**
  149.      * {@inheritDoc}
  150.      */
  151.     @Override
  152.     public boolean containsVariable(String name) {
  153.       return variableNames.contains(name);
  154.     }

  155.     /**
  156.      * {@inheritDoc}
  157.      */
  158.     @Override
  159.     public Set<String> getVariableNames() {
  160.       return variableNames;
  161.     }

  162.     /**
  163.      * {@inheritDoc}
  164.      */
  165.     @Override
  166.     public Object getVariable(String name) {
  167.       if (dynamicContext.getBindings().containsKey(name)) {
  168.         return dynamicContext.getBindings().get(name);
  169.       }
  170.       if (configurationProperties != null && configurationProperties.containsKey(name)) {
  171.         return configurationProperties.getProperty(name);
  172.       }
  173.       return getParameterValue(name);
  174.     }

  175.     abstract Object getParameterValue(String name);

  176.   }

  177.   private static class MapBasedContext extends AbstractContext {

  178.     private final Map<String, Object> variables;

  179.     private MapBasedContext(Map<String, Object> parameterMap, DynamicContext dynamicContext,
  180.         Properties configurationProperties) {
  181.       super(dynamicContext, configurationProperties);
  182.       this.variables = parameterMap;
  183.       addVariableNames(parameterMap.keySet());
  184.     }

  185.     /**
  186.      * {@inheritDoc}
  187.      */
  188.     @Override
  189.     public Object getParameterValue(String name) {
  190.       return variables.get(name);
  191.     }

  192.   }

  193.   private static class MetaClassBasedContext extends AbstractContext {

  194.     private final Object parameterObject;
  195.     private final MetaClass parameterMetaClass;
  196.     private final Class<?> parameterType;

  197.     private MetaClassBasedContext(Object parameterObject, MetaClass parameterMetaClass, Class<?> parameterType,
  198.         DynamicContext dynamicContext, Properties configurationProperties) {
  199.       super(dynamicContext, configurationProperties);
  200.       this.parameterObject = parameterObject;
  201.       this.parameterMetaClass = parameterMetaClass;
  202.       this.parameterType = parameterType;
  203.       addVariableNames(Arrays.asList(parameterMetaClass.getGetterNames()));
  204.     }

  205.     /**
  206.      * {@inheritDoc}
  207.      */
  208.     @Override
  209.     public Object getParameterValue(String name) {
  210.       try {
  211.         return parameterMetaClass.getGetInvoker(name).invoke(parameterObject, null);
  212.       } catch (IllegalAccessException | InvocationTargetException e) {
  213.         throw new IllegalStateException(
  214.             String.format("Cannot get a value for property named '%s' in '%s'", name, parameterType), e);
  215.       }
  216.     }

  217.   }

  218. }