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