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.util.Arrays;
19  import java.util.Collections;
20  import java.util.HashMap;
21  import java.util.HashSet;
22  import java.util.Locale;
23  import java.util.Map;
24  import java.util.Optional;
25  import java.util.Set;
26  import java.util.function.BiConsumer;
27  import java.util.function.BiFunction;
28  import java.util.stream.Collectors;
29  
30  import org.mybatis.scripting.thymeleaf.expression.Likes;
31  import org.thymeleaf.ITemplateEngine;
32  import org.thymeleaf.TemplateEngine;
33  import org.thymeleaf.context.IContext;
34  import org.thymeleaf.templatemode.TemplateMode;
35  import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
36  import org.thymeleaf.templateresolver.StringTemplateResolver;
37  
38  /**
39   * The sql template engine for integrating with Thymeleaf.
40   *
41   * @author Kazuki Shimizu
42   *
43   * @version 1.0.2
44   */
45  public class SqlGenerator {
46  
47    static class ContextKeys {
48      static final String PARAMETER_OBJECT = "_parameter";
49    }
50  
51    private final ITemplateEngine templateEngine;
52    private Map<String, Object> defaultCustomVariables = Collections.emptyMap();
53    private PropertyAccessor propertyAccessor = PropertyAccessor.BuiltIn.STANDARD;
54    private BiFunction<Object, Map<String, Object>, IContext> contextFactory = DefaultContext::new;
55  
56    /**
57     * Constructor for creating instance with default {@code TemplateEngine}.
58     */
59    public SqlGenerator() {
60      this.templateEngine = createDefaultTemplateEngine(SqlGeneratorConfig.newInstance());
61    }
62  
63    /**
64     * Constructor for creating instance with user specified {@link SqlGenerator}.
65     *
66     * @param config
67     *          A user defined {@link SqlGeneratorConfig} instance
68     */
69    public SqlGenerator(SqlGeneratorConfig config) {
70      this.templateEngine = createDefaultTemplateEngine(config);
71    }
72  
73    /**
74     * Constructor for creating instance with user defined {@code ITemplateEngine}.
75     *
76     * @param templateEngine
77     *          A user defined {@code ITemplateEngine} instance
78     */
79    public SqlGenerator(ITemplateEngine templateEngine) {
80      this.templateEngine = templateEngine;
81    }
82  
83    /**
84     * Set default custom variables.
85     *
86     * @param defaultCustomVariables
87     *          a default custom variables for passing to template engine
88     */
89    public void setDefaultCustomVariables(Map<String, Object> defaultCustomVariables) {
90      this.defaultCustomVariables = Optional.ofNullable(defaultCustomVariables).map(Collections::unmodifiableMap)
91          .orElseGet(Collections::emptyMap);
92    }
93  
94    /**
95     * Get specified default custom variables.
96     *
97     * @return specified default custom variables
98     */
99    public Map<String, Object> getDefaultCustomVariables() {
100     return defaultCustomVariables;
101   }
102 
103   /**
104    * Set a property accessor.
105    * <p>
106    * Default is {@link PropertyAccessor.BuiltIn#STANDARD}.
107    * </p>
108    *
109    * @param propertyAccessor
110    *          a property accessor
111    */
112   public void setPropertyAccessor(PropertyAccessor propertyAccessor) {
113     this.propertyAccessor = Optional.ofNullable(propertyAccessor).orElse(PropertyAccessor.BuiltIn.STANDARD);
114   }
115 
116   /**
117    * Set a factory function for creating instance of custom context.
118    *
119    * @param contextFactory
120    *          a factory function
121    */
122   void setContextFactory(BiFunction<Object, Map<String, Object>, IContext> contextFactory) {
123     this.contextFactory = contextFactory;
124   }
125 
126   private ITemplateEngine createDefaultTemplateEngine(SqlGeneratorConfig config) {
127     MyBatisDialect dialect = new MyBatisDialect(config.getDialect().getPrefix());
128     Optional.ofNullable(config.getDialect().getBindVariableRenderInstance()).ifPresent(dialect::setBindVariableRender);
129     Likes likes = Likes.newBuilder().escapeChar(config.getDialect().getLikeEscapeChar())
130         .escapeClauseFormat(config.getDialect().getLikeEscapeClauseFormat())
131         .additionalEscapeTargetChars(config.getDialect().getLikeAdditionalEscapeTargetChars()).build();
132     dialect.setLikes(likes);
133 
134     // Create an ClassLoaderTemplateResolver instance
135     ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
136     TemplateMode mode = config.isUse2way() ? TemplateMode.CSS : TemplateMode.TEXT;
137     classLoaderTemplateResolver.setOrder(1);
138     classLoaderTemplateResolver.setTemplateMode(mode);
139     classLoaderTemplateResolver
140         .setResolvablePatterns(Arrays.stream(config.getTemplateFile().getPatterns()).collect(Collectors.toSet()));
141     classLoaderTemplateResolver.setCharacterEncoding(config.getTemplateFile().getEncoding().name());
142     classLoaderTemplateResolver.setCacheable(config.getTemplateFile().isCacheEnabled());
143     classLoaderTemplateResolver.setCacheTTLMs(config.getTemplateFile().getCacheTtl());
144     classLoaderTemplateResolver.setPrefix(config.getTemplateFile().getBaseDir());
145 
146     // Create an StringTemplateResolver instance
147     StringTemplateResolver stringTemplateResolver = new StringTemplateResolver();
148     stringTemplateResolver.setOrder(2);
149     stringTemplateResolver.setTemplateMode(mode);
150 
151     // Create an TemplateEngine instance
152     TemplateEngine targetTemplateEngine = new TemplateEngine();
153     targetTemplateEngine.addTemplateResolver(classLoaderTemplateResolver);
154     targetTemplateEngine.addTemplateResolver(stringTemplateResolver);
155     targetTemplateEngine.addDialect(dialect);
156     targetTemplateEngine.setEngineContextFactory(
157         new MyBatisIntegratingEngineContextFactory(targetTemplateEngine.getEngineContextFactory()));
158 
159     // Create an TemplateEngineCustomizer instance and apply
160     Optional.ofNullable(config.getCustomizerInstance()).ifPresent(x -> x.accept(targetTemplateEngine));
161 
162     return targetTemplateEngine;
163   }
164 
165   /**
166    * Generate a sql using Thymeleaf template engine.
167    *
168    * @param sqlTemplate
169    *          a template SQL
170    * @param parameter
171    *          a parameter object
172    *
173    * @return a processed SQL by template engine
174    */
175   public String generate(CharSequence sqlTemplate, Object parameter) {
176     return generate(sqlTemplate, parameter, null, null);
177   }
178 
179   /**
180    * Generate a sql using Thymeleaf template engine.
181    *
182    * @param sqlTemplate
183    *          a template SQL
184    * @param parameter
185    *          a parameter object
186    * @param customBindVariableBinder
187    *          a binder for a custom bind variable that generated with {@code mb:bind} or {@code mb:param}
188    *
189    * @return a processed SQL by template engine
190    */
191   public String generate(CharSequence sqlTemplate, Object parameter,
192       BiConsumer<String, Object> customBindVariableBinder) {
193     return generate(sqlTemplate, parameter, customBindVariableBinder, null);
194   }
195 
196   /**
197    * Generate a sql using Thymeleaf template engine.
198    *
199    * @param sqlTemplate
200    *          a template SQL
201    * @param parameter
202    *          a parameter object
203    * @param customVariables
204    *          a custom variables for passing to template engine
205    *
206    * @return a processed SQL by template engine
207    */
208   public String generate(CharSequence sqlTemplate, Object parameter, Map<String, Object> customVariables) {
209     return generate(sqlTemplate, parameter, null, customVariables);
210   }
211 
212   /**
213    * Generate a sql using Thymeleaf template engine.
214    *
215    * @param sqlTemplate
216    *          a template SQL
217    * @param parameter
218    *          a parameter object
219    * @param customBindVariableBinder
220    *          a binder for a custom bind variable that generated with {@code mb:bind} or {@code mb:param}
221    * @param customVariables
222    *          a custom variables for passing to template engine
223    *
224    * @return a processed SQL by template engine
225    */
226   public String generate(CharSequence sqlTemplate, Object parameter,
227       BiConsumer<String, Object> customBindVariableBinder, Map<String, Object> customVariables) {
228 
229     Map<String, Object> processingCustomVariables = new HashMap<>(defaultCustomVariables);
230     Optional.ofNullable(customVariables).ifPresent(processingCustomVariables::putAll);
231 
232     IContext context = contextFactory.apply(parameter, processingCustomVariables);
233     String sql = templateEngine.process(sqlTemplate.toString(), context);
234 
235     MyBatisBindingContext bindingContext = MyBatisBindingContext.load(context);
236     if (bindingContext != null && customBindVariableBinder != null) {
237       bindingContext.getCustomBindVariables().forEach(customBindVariableBinder);
238     }
239 
240     return sql;
241   }
242 
243   private class DefaultContext implements IContext {
244 
245     private final Object parameter;
246     private final Map<String, Object> mapParameter;
247     private final Set<String> propertyNames = new HashSet<>();
248     private final Map<String, Object> customVariables;
249 
250     private DefaultContext(Object parameter, Map<String, Object> customVariables) {
251       this.parameter = parameter;
252       boolean fallback;
253       if (parameter instanceof Map) {
254         @SuppressWarnings("unchecked")
255         Map<String, Object> map = (Map<String, Object>) parameter;
256         propertyNames.addAll(map.keySet());
257         this.mapParameter = map;
258         fallback = false;
259       } else {
260         this.mapParameter = null;
261         if (parameter != null) {
262           propertyNames.addAll(propertyAccessor.getPropertyNames(parameter.getClass()));
263         }
264         fallback = propertyNames.isEmpty();
265       }
266       MyBatisBindingContext bindingContext = new MyBatisBindingContext(fallback);
267       this.customVariables = customVariables;
268       customVariables.put(MyBatisBindingContext.CONTEXT_VARIABLE_NAME, bindingContext);
269       customVariables.put(ContextKeys.PARAMETER_OBJECT, parameter);
270     }
271 
272     @Override
273     public Locale getLocale() {
274       return Locale.getDefault();
275     }
276 
277     @Override
278     public boolean containsVariable(String name) {
279       return customVariables.containsKey(name) || propertyNames.contains(name);
280     }
281 
282     @Override
283     public Set<String> getVariableNames() {
284       Set<String> variableNames = new HashSet<>(customVariables.keySet());
285       variableNames.addAll(propertyNames);
286       return variableNames;
287     }
288 
289     @Override
290     public Object getVariable(String name) {
291       if (customVariables.containsKey(name)) {
292         return customVariables.get(name);
293       }
294       if (mapParameter == null) {
295         return propertyAccessor.getPropertyValue(parameter, name);
296       } else {
297         return mapParameter.get(name);
298       }
299     }
300 
301   }
302 
303 }