View Javadoc
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  
18  import java.lang.reflect.InvocationTargetException;
19  import java.util.Arrays;
20  import java.util.Collection;
21  import java.util.HashSet;
22  import java.util.Locale;
23  import java.util.Map;
24  import java.util.Optional;
25  import java.util.Properties;
26  import java.util.Set;
27  import java.util.function.BiFunction;
28  
29  import org.apache.ibatis.builder.SqlSourceBuilder;
30  import org.apache.ibatis.mapping.BoundSql;
31  import org.apache.ibatis.mapping.SqlSource;
32  import org.apache.ibatis.reflection.MetaClass;
33  import org.apache.ibatis.scripting.xmltags.DynamicContext;
34  import org.apache.ibatis.session.Configuration;
35  import org.thymeleaf.context.IContext;
36  
37  /**
38   * The {@code SqlSource} for integrating with Thymeleaf.
39   *
40   * @author Kazuki Shimizu
41   *
42   * @version 1.0.0
43   *
44   * @see ThymeleafLanguageDriver
45   */
46  class ThymeleafSqlSource implements SqlSource {
47  
48    private static class TemporaryTakeoverKeys {
49      private static final String CONFIGURATION = "__configuration__";
50      private static final String DYNAMIC_CONTEXT = "__dynamicContext__";
51      private static final String PROCESSING_PARAMETER_TYPE = "__processingParameterType__";
52    }
53  
54    private final Configuration configuration;
55    private final SqlGenerator sqlGenerator;
56    private final SqlSourceBuilder sqlSourceBuilder;
57    private final String sqlTemplate;
58    private final Class<?> parameterType;
59  
60    /**
61     * Constructor for for integrating with template engine provide by Thymeleaf.
62     *
63     * @param configuration
64     *          A configuration instance of MyBatis
65     * @param sqlGenerator
66     *          A sql generator using the Thymeleaf feature
67     * @param sqlTemplate
68     *          A template string of SQL (inline SQL or template file path)
69     * @param parameterType
70     *          A parameter type that specified at mapper method argument or xml element
71     */
72    ThymeleafSqlSource(Configuration configuration, SqlGenerator sqlGenerator, String sqlTemplate,
73        Class<?> parameterType) {
74      this.configuration = configuration;
75      this.sqlGenerator = sqlGenerator;
76      this.sqlTemplate = sqlTemplate;
77      this.parameterType = parameterType;
78      this.sqlSourceBuilder = new SqlSourceBuilder(configuration);
79    }
80  
81    /**
82     * {@inheritDoc}
83     */
84    @Override
85    public BoundSql getBoundSql(Object parameterObject) {
86      Class<?> processingParameterType;
87      if (parameterType == null) {
88        processingParameterType = parameterObject == null ? Object.class : parameterObject.getClass();
89      } else {
90        processingParameterType = parameterType;
91      }
92  
93      DynamicContext dynamicContext = new DynamicContext(configuration, parameterObject);
94      Map<String, Object> customVariables = dynamicContext.getBindings();
95      customVariables.put(TemporaryTakeoverKeys.CONFIGURATION, configuration);
96      customVariables.put(TemporaryTakeoverKeys.DYNAMIC_CONTEXT, dynamicContext);
97      customVariables.put(TemporaryTakeoverKeys.PROCESSING_PARAMETER_TYPE, processingParameterType);
98      String sql = sqlGenerator.generate(sqlTemplate, parameterObject, dynamicContext::bind, customVariables);
99  
100     SqlSource sqlSource = sqlSourceBuilder.parse(sql, processingParameterType, dynamicContext.getBindings());
101     BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
102     dynamicContext.getBindings().forEach(boundSql::setAdditionalParameter);
103 
104     return boundSql;
105   }
106 
107   /**
108    * The factory class for Thymeleaf's context.
109    *
110    * @since 1.0.2
111    */
112   static class ContextFactory implements BiFunction<Object, Map<String, Object>, IContext> {
113     /**
114      * {@inheritDoc}
115      */
116     @Override
117     public IContext apply(Object parameter, Map<String, Object> customVariable) {
118       Configuration configuration = (Configuration) customVariable.remove(TemporaryTakeoverKeys.CONFIGURATION);
119       DynamicContext dynamicContext = (DynamicContext) customVariable.remove(TemporaryTakeoverKeys.DYNAMIC_CONTEXT);
120       Class<?> processingParameterType = (Class<?>) customVariable
121           .remove(TemporaryTakeoverKeys.PROCESSING_PARAMETER_TYPE);
122       MyBatisBindingContext bindingContext = new MyBatisBindingContext(
123           parameter != null && configuration.getTypeHandlerRegistry().hasTypeHandler(processingParameterType));
124       dynamicContext.bind(MyBatisBindingContext.CONTEXT_VARIABLE_NAME, bindingContext);
125       IContext context;
126       if (parameter instanceof Map) {
127         @SuppressWarnings(value = "unchecked")
128         Map<String, Object> map = (Map<String, Object>) parameter;
129         context = new MapBasedContext(map, dynamicContext, configuration.getVariables());
130       } else {
131         MetaClass metaClass = MetaClass.forClass(processingParameterType, configuration.getReflectorFactory());
132         context = new MetaClassBasedContext(parameter, metaClass, processingParameterType, dynamicContext,
133             configuration.getVariables());
134       }
135       return context;
136     }
137   }
138 
139   private abstract static class AbstractContext implements IContext {
140 
141     private final DynamicContext dynamicContext;
142     private final Properties configurationProperties;
143     private final Set<String> variableNames;
144 
145     private AbstractContext(DynamicContext dynamicContext, Properties configurationProperties) {
146       this.dynamicContext = dynamicContext;
147       this.configurationProperties = configurationProperties;
148       this.variableNames = new HashSet<>();
149       addVariableNames(dynamicContext.getBindings().keySet());
150       Optional.ofNullable(configurationProperties).ifPresent(v -> addVariableNames(v.stringPropertyNames()));
151     }
152 
153     void addVariableNames(Collection<String> names) {
154       variableNames.addAll(names);
155     }
156 
157     /**
158      * {@inheritDoc}
159      */
160     @Override
161     public Locale getLocale() {
162       return Locale.getDefault();
163     }
164 
165     /**
166      * {@inheritDoc}
167      */
168     @Override
169     public boolean containsVariable(String name) {
170       return variableNames.contains(name);
171     }
172 
173     /**
174      * {@inheritDoc}
175      */
176     @Override
177     public Set<String> getVariableNames() {
178       return variableNames;
179     }
180 
181     /**
182      * {@inheritDoc}
183      */
184     @Override
185     public Object getVariable(String name) {
186       if (dynamicContext.getBindings().containsKey(name)) {
187         return dynamicContext.getBindings().get(name);
188       }
189       if (configurationProperties != null && configurationProperties.containsKey(name)) {
190         return configurationProperties.getProperty(name);
191       }
192       return getParameterValue(name);
193     }
194 
195     abstract Object getParameterValue(String name);
196 
197   }
198 
199   private static class MapBasedContext extends AbstractContext {
200 
201     private final Map<String, Object> variables;
202 
203     private MapBasedContext(Map<String, Object> parameterMap, DynamicContext dynamicContext,
204         Properties configurationProperties) {
205       super(dynamicContext, configurationProperties);
206       this.variables = parameterMap;
207       addVariableNames(parameterMap.keySet());
208     }
209 
210     /**
211      * {@inheritDoc}
212      */
213     @Override
214     public Object getParameterValue(String name) {
215       return variables.get(name);
216     }
217 
218   }
219 
220   private static class MetaClassBasedContext extends AbstractContext {
221 
222     private final Object parameterObject;
223     private final MetaClass parameterMetaClass;
224     private final Class<?> parameterType;
225 
226     private MetaClassBasedContext(Object parameterObject, MetaClass parameterMetaClass, Class<?> parameterType,
227         DynamicContext dynamicContext, Properties configurationProperties) {
228       super(dynamicContext, configurationProperties);
229       this.parameterObject = parameterObject;
230       this.parameterMetaClass = parameterMetaClass;
231       this.parameterType = parameterType;
232       addVariableNames(Arrays.asList(parameterMetaClass.getGetterNames()));
233     }
234 
235     /**
236      * {@inheritDoc}
237      */
238     @Override
239     public Object getParameterValue(String name) {
240       try {
241         return parameterMetaClass.getGetInvoker(name).invoke(parameterObject, null);
242       } catch (IllegalAccessException | InvocationTargetException e) {
243         throw new IllegalStateException(
244             String.format("Cannot get a value for property named '%s' in '%s'", name, parameterType), e);
245       }
246     }
247 
248   }
249 
250 }