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.io.BufferedReader;
19  import java.io.IOException;
20  import java.io.InputStreamReader;
21  import java.lang.reflect.InvocationTargetException;
22  import java.nio.charset.Charset;
23  import java.nio.charset.StandardCharsets;
24  import java.util.Arrays;
25  import java.util.HashMap;
26  import java.util.Map;
27  import java.util.Optional;
28  import java.util.Properties;
29  import java.util.function.Consumer;
30  import java.util.function.Function;
31  import java.util.stream.Stream;
32  
33  import org.mybatis.scripting.thymeleaf.PropertyAccessor.BuiltIn.StandardPropertyAccessor;
34  import org.mybatis.scripting.thymeleaf.processor.BindVariableRender;
35  import org.thymeleaf.util.ClassLoaderUtils;
36  import org.thymeleaf.util.StringUtils;
37  
38  /**
39   * Configuration class for {@link SqlGenerator}.
40   *
41   * @author Kazuki Shimizu
42   *
43   * @since 1.0.2
44   */
45  public class SqlGeneratorConfig {
46  
47    private static class PropertyKeys {
48      private static final String CONFIG_FILE = "mybatis-thymeleaf.config.file";
49      private static final String CONFIG_ENCODING = "mybatis-thymeleaf.config.encoding";
50    }
51  
52    private static class Defaults {
53      private static final String PROPERTIES_FILE = "mybatis-thymeleaf.properties";
54    }
55  
56    private static final Map<Class<?>, Function<String, Object>> TYPE_CONVERTERS;
57  
58    static {
59      Map<Class<?>, Function<String, Object>> converters = new HashMap<>();
60      converters.put(boolean.class, v -> Boolean.valueOf(v.trim()));
61      converters.put(String.class, String::trim);
62      converters.put(Character[].class, v -> Stream.of(v.split(",")).map(String::trim).filter(e -> e.length() == 1)
63          .map(e -> e.charAt(0)).toArray(Character[]::new));
64      converters.put(Character.class, v -> v.trim().charAt(0));
65      converters.put(Charset.class, v -> Charset.forName(v.trim()));
66      converters.put(Long.class, v -> Long.valueOf(v.trim()));
67      converters.put(String[].class, v -> Stream.of(v.split(",")).map(String::trim).toArray(String[]::new));
68      converters.put(Class.class, SqlGeneratorConfig::toClassForName);
69      TYPE_CONVERTERS = Map.copyOf(converters);
70    }
71  
72    /**
73     * Whether use the 2-way SQL feature.
74     */
75    private boolean use2way = true;
76  
77    /**
78     * The instance for customizing a default TemplateEngine instanced by the mybatis-thymeleaf.
79     */
80    private TemplateEngineCustomizer customizer;
81  
82    /**
83     * Template file configuration.
84     */
85    private final TemplateFileConfig templateFile = new TemplateFileConfig();
86  
87    /**
88     * Dialect configuration.
89     */
90    private final DialectConfig dialect = new DialectConfig();
91  
92    /**
93     * Get whether use the 2-way SQL feature.
94     * <p>
95     * Default is {@code true}.
96     * </p>
97     *
98     * @return If use the 2-way SQL feature, return {@code true}
99     */
100   public boolean isUse2way() {
101     return use2way;
102   }
103 
104   /**
105    * Set whether use the 2-way SQL feature.
106    *
107    * @param use2way
108    *          If use the 2-way SQL feature, set {@code true}
109    */
110   public void setUse2way(boolean use2way) {
111     this.use2way = use2way;
112   }
113 
114   /**
115    * Get the interface for customizing a default TemplateEngine instanced by the mybatis-thymeleaf.
116    * <p>
117    * Default is {@code null}.
118    * </p>
119    * This method exists for the backward compatibility.<br>
120    * Use {@link #getCustomizerInstance()} instead
121    *
122    * @return the interface for customizing a default TemplateEngine
123    */
124   @Deprecated
125   public Class<? extends TemplateEngineCustomizer> getCustomizer() {
126     return customizer == null ? null : customizer.getClass();
127   }
128 
129   /**
130    * Set the interface for customizing a default TemplateEngine instanced by the mybatis-thymeleaf.
131    *
132    * @param customizer
133    *          the interface for customizing a default TemplateEngine
134    */
135   @Deprecated
136   public void setCustomizer(Class<? extends TemplateEngineCustomizer> customizer) {
137     this.customizer = newInstanceForType(customizer);
138   }
139 
140   public TemplateEngineCustomizer getCustomizerInstance() {
141     return customizer;
142   }
143 
144   public void setCustomizerInstance(TemplateEngineCustomizer customizer) {
145     this.customizer = customizer;
146   }
147 
148   /**
149    * Get a template file configuration.
150    *
151    * @return a template file configuration
152    */
153   public TemplateFileConfig getTemplateFile() {
154     return templateFile;
155   }
156 
157   /**
158    * Get a dialect configuration.
159    *
160    * @return a dialect configuration
161    */
162   public DialectConfig getDialect() {
163     return dialect;
164   }
165 
166   /**
167    * Template file configuration.
168    *
169    * @since 1.0.0
170    */
171   public static class TemplateFileConfig {
172 
173     /**
174      * The character encoding for reading template resource file.
175      */
176     private Charset encoding = StandardCharsets.UTF_8;
177 
178     /**
179      * The base directory for reading template resource file.
180      */
181     private String baseDir = "";
182 
183     /**
184      * The patterns for reading as template resource file. (Can specify multiple patterns using comma(",") as separator
185      * character)
186      */
187     private String[] patterns = { "*.sql" };
188 
189     /**
190      * Whether use the cache feature when load template resource file.
191      */
192     private boolean cacheEnabled = true;
193 
194     /**
195      * The cache TTL(millisecond) for resolved templates.
196      */
197     private Long cacheTtl;
198 
199     /**
200      * Get the character encoding for reading template resource file.
201      * <p>
202      * Default is {@code UTF-8}.
203      * </p>
204      *
205      * @return the character encoding for reading template resource file
206      */
207     public Charset getEncoding() {
208       return encoding;
209     }
210 
211     /**
212      * Set the character encoding for reading template resource file.
213      *
214      * @param encoding
215      *          the character encoding for reading template resource file
216      */
217     public void setEncoding(Charset encoding) {
218       this.encoding = encoding;
219     }
220 
221     /**
222      * Get the base directory for reading template resource file.
223      * <p>
224      * Default is {@code ""}(none).
225      * </p>
226      *
227      * @return the base directory for reading template resource file
228      */
229     public String getBaseDir() {
230       return baseDir;
231     }
232 
233     /**
234      * Set the base directory for reading template resource file.
235      *
236      * @param baseDir
237      *          the base directory for reading template resource file
238      */
239     public void setBaseDir(String baseDir) {
240       this.baseDir = baseDir;
241     }
242 
243     /**
244      * Get patterns for reading as template resource file.
245      * <p>
246      * Default is {@code "*.sql"}.
247      * </p>
248      *
249      * @return patterns for reading as template resource file
250      */
251     public String[] getPatterns() {
252       return patterns;
253     }
254 
255     /**
256      * Set patterns for reading as template resource file.
257      *
258      * @param patterns
259      *          patterns for reading as template resource file
260      */
261     public void setPatterns(String... patterns) {
262       this.patterns = patterns;
263     }
264 
265     /**
266      * Get whether use the cache feature when load template resource file.
267      * <p>
268      * Default is {@code true}.
269      * </p>
270      *
271      * @return If use th cache feature, return {@code true}
272      */
273     public boolean isCacheEnabled() {
274       return cacheEnabled;
275     }
276 
277     /**
278      * Set whether use the cache feature when load template resource file.
279      *
280      * @param cacheEnabled
281      *          If use th cache feature, set {@code true}
282      */
283     public void setCacheEnabled(boolean cacheEnabled) {
284       this.cacheEnabled = cacheEnabled;
285     }
286 
287     /**
288      * Get the cache TTL(millisecond) for resolved templates.
289      * <p>
290      * Default is {@code null}(indicate to use default value of Thymeleaf).
291      * </p>
292      *
293      * @return the cache TTL(millisecond) for resolved templates
294      */
295     public Long getCacheTtl() {
296       return cacheTtl;
297     }
298 
299     /**
300      * Set the cache TTL(millisecond) for resolved templates.
301      *
302      * @param cacheTtl
303      *          the cache TTL(millisecond) for resolved templates
304      */
305     public void setCacheTtl(Long cacheTtl) {
306       this.cacheTtl = cacheTtl;
307     }
308 
309   }
310 
311   /**
312    * Dialect configuration.
313    *
314    * @since 1.0.0
315    */
316   public static class DialectConfig {
317 
318     /**
319      * The prefix name of dialect provided by this project.
320      */
321     private String prefix = "mb";
322 
323     /**
324      * The escape character for wildcard of LIKE condition.
325      */
326     private Character likeEscapeChar = '\\';
327 
328     /**
329      * The format of escape clause for LIKE condition (Can specify format that can be allowed by String#format method).
330      */
331     private String likeEscapeClauseFormat = "ESCAPE '%s'";
332 
333     /**
334      * Additional escape target characters(custom wildcard characters) for LIKE condition. (Can specify multiple
335      * characters using comma(",") as separator character)
336      */
337     private Character[] likeAdditionalEscapeTargetChars;
338 
339     /**
340      * The bind variable render.
341      */
342     private BindVariableRender bindVariableRender;
343 
344     /**
345      * Get the prefix name of dialect provided by this project.
346      * <p>
347      * Default is {@code "mb"}.
348      * </p>
349      *
350      * @return the prefix name of dialect
351      */
352     public String getPrefix() {
353       return prefix;
354     }
355 
356     /**
357      * Set the prefix name of dialect provided by this project.
358      *
359      * @param prefix
360      *          the prefix name of dialect
361      */
362     public void setPrefix(String prefix) {
363       this.prefix = prefix;
364     }
365 
366     /**
367      * Get the escape character for wildcard of LIKE condition.
368      * <p>
369      * Default is {@code '\'}.
370      * </p>
371      *
372      * @return the escape character for wildcard
373      */
374     public Character getLikeEscapeChar() {
375       return likeEscapeChar;
376     }
377 
378     /**
379      * Set the escape character for wildcard of LIKE condition.
380      *
381      * @param likeEscapeChar
382      *          the escape character for wildcard
383      */
384     public void setLikeEscapeChar(Character likeEscapeChar) {
385       this.likeEscapeChar = likeEscapeChar;
386     }
387 
388     /**
389      * Get the format of escape clause for LIKE condition.
390      * <p>
391      * Can specify format that can be allowed by String#format method. Default is {@code "ESCAPE '%s'"}.
392      * </p>
393      *
394      * @return the format of escape clause for LIKE condition
395      */
396     public String getLikeEscapeClauseFormat() {
397       return likeEscapeClauseFormat;
398     }
399 
400     /**
401      * Set the format of escape clause for LIKE condition.
402      *
403      * @param likeEscapeClauseFormat
404      *          the format of escape clause for LIKE condition
405      */
406     public void setLikeEscapeClauseFormat(String likeEscapeClauseFormat) {
407       this.likeEscapeClauseFormat = likeEscapeClauseFormat;
408     }
409 
410     /**
411      * Get additional escape target characters(custom wildcard characters) for LIKE condition.
412      * <p>
413      * Can specify multiple characters using comma(",") as separator character. Default is empty(none).
414      * </p>
415      *
416      * @return additional escape target characters(custom wildcard characters)
417      */
418     public Character[] getLikeAdditionalEscapeTargetChars() {
419       return likeAdditionalEscapeTargetChars;
420     }
421 
422     /**
423      * Set additional escape target characters(custom wildcard characters) for LIKE condition.
424      *
425      * @param likeAdditionalEscapeTargetChars
426      *          additional escape target characters(custom wildcard characters)
427      */
428     public void setLikeAdditionalEscapeTargetChars(Character... likeAdditionalEscapeTargetChars) {
429       this.likeAdditionalEscapeTargetChars = likeAdditionalEscapeTargetChars;
430     }
431 
432     /**
433      * Get a bind variable render.
434      * <p>
435      * Default is {@link BindVariableRender.BuiltIn#MYBATIS}
436      * </p>
437      * This method exists for the backward compatibility. <br>
438      * Use {@link #getBindVariableRenderInstance()} instead
439      *
440      * @return a bind variable render
441      */
442     @Deprecated
443     public Class<? extends BindVariableRender> getBindVariableRender() {
444       return bindVariableRender == null ? null : bindVariableRender.getClass();
445     }
446 
447     /**
448      * This method exists for the backward compatibility.<br>
449      * Use {@link #setBindVariableRenderInstance(BindVariableRender)} instead
450      *
451      * @param bindVariableRender
452      *          bindVariableRender class
453      */
454     @Deprecated
455     public void setBindVariableRender(Class<? extends BindVariableRender> bindVariableRender) {
456       this.bindVariableRender = newInstanceForType(bindVariableRender);
457     }
458 
459     public BindVariableRender getBindVariableRenderInstance() {
460       return bindVariableRender;
461     }
462 
463     public void setBindVariableRenderInstance(BindVariableRender bindVariableRender) {
464       this.bindVariableRender = bindVariableRender;
465     }
466   }
467 
468   /**
469    * Create an instance from default properties file. <br>
470    * If you want to customize a default {@code TemplateEngine}, you can configure some property using
471    * mybatis-thymeleaf.properties that encoded by UTF-8. Also, you can change the properties file that will read using
472    * system property (-Dmybatis-thymeleaf.config.file=... -Dmybatis-thymeleaf.config.encoding=...). <br>
473    * Supported properties are as follows:
474    * <table border="1">
475    * <caption>Supported properties</caption>
476    * <tr>
477    * <th>Property Key</th>
478    * <th>Description</th>
479    * <th>Default</th>
480    * </tr>
481    * <tr>
482    * <th colspan="3">General configuration</th>
483    * </tr>
484    * <tr>
485    * <td>use2way</td>
486    * <td>Whether use the 2-way SQL</td>
487    * <td>{@code true}</td>
488    * </tr>
489    * <tr>
490    * <td>customizer</td>
491    * <td>The implementation class for customizing a default {@code TemplateEngine} instanced by the MyBatis Thymeleaf
492    * </td>
493    * <td>None</td>
494    * </tr>
495    * <tr>
496    * <th colspan="3">Template file configuration</th>
497    * </tr>
498    * <tr>
499    * <td>template-file.cache-enabled</td>
500    * <td>Whether use the cache feature</td>
501    * <td>{@code true}</td>
502    * </tr>
503    * <tr>
504    * <td>template-file.cache-ttl</td>
505    * <td>The cache TTL for resolved templates</td>
506    * <td>None(use default value of Thymeleaf)</td>
507    * </tr>
508    * <tr>
509    * <td>template-file.encoding</td>
510    * <td>The character encoding for reading template resources</td>
511    * <td>{@code "UTF-8"}</td>
512    * </tr>
513    * <tr>
514    * <td>template-file.base-dir</td>
515    * <td>The base directory for reading template resources</td>
516    * <td>None(just under class path)</td>
517    * </tr>
518    * <tr>
519    * <td>template-file.patterns</td>
520    * <td>The patterns for reading as template resources</td>
521    * <td>{@code "*.sql"}</td>
522    * </tr>
523    * <tr>
524    * <th colspan="3">Dialect configuration</th>
525    * </tr>
526    * <tr>
527    * <td>dialect.prefix</td>
528    * <td>The prefix name of dialect provided by this project</td>
529    * <td>{@code "mb"}</td>
530    * </tr>
531    * <tr>
532    * <td>dialect.like-escape-char</td>
533    * <td>The escape character for wildcard of LIKE</td>
534    * <td>{@code '\'} (backslash)</td>
535    * </tr>
536    * <tr>
537    * <td>dialect.like-escape-clause-format</td>
538    * <td>The format of escape clause</td>
539    * <td>{@code "ESCAPE '%s'"}</td>
540    * </tr>
541    * <tr>
542    * <td>dialect.like-additional-escape-target-chars</td>
543    * <td>The additional escape target characters(custom wildcard characters) for LIKE condition</td>
544    * <td>None</td>
545    * </tr>
546    * </table>
547    *
548    * @return a configuration instance
549    */
550   public static SqlGeneratorConfig newInstance() {
551     SqlGeneratorConfig config = new SqlGeneratorConfig();
552     applyDefaultProperties(config);
553     return config;
554   }
555 
556   /**
557    * Create an instance from specified properties file. <br>
558    * you can configure some property using specified properties file that encoded by UTF-8. Also, you can change file
559    * encoding that will read using system property (-Dmybatis-thymeleaf.config.encoding=...).
560    *
561    * @param resourcePath
562    *          A property file resource path
563    *
564    * @return a configuration instance
565    *
566    * @see #newInstance()
567    */
568   public static SqlGeneratorConfig newInstanceWithResourcePath(String resourcePath) {
569     SqlGeneratorConfig config = new SqlGeneratorConfig();
570     applyResourcePath(config, resourcePath);
571     return config;
572   }
573 
574   /**
575    * Create an instance from specified properties.
576    *
577    * @param customProperties
578    *          custom configuration properties
579    *
580    * @return a configuration instance
581    *
582    * @see #newInstance()
583    */
584   public static SqlGeneratorConfig newInstanceWithProperties(Properties customProperties) {
585     SqlGeneratorConfig config = new SqlGeneratorConfig();
586     applyProperties(config, customProperties);
587     return config;
588   }
589 
590   /**
591    * Create an instance using specified customizer and override using a default properties file.
592    *
593    * @param customizer
594    *          baseline customizer
595    *
596    * @return a configuration instance
597    *
598    * @see #newInstance()
599    */
600   public static SqlGeneratorConfig newInstanceWithCustomizer(Consumer<SqlGeneratorConfig> customizer) {
601     SqlGeneratorConfig config = new SqlGeneratorConfig();
602     customizer.accept(config);
603     applyDefaultProperties(config);
604     return config;
605   }
606 
607   /**
608    * Apply properties that read from default properties file. <br>
609    * If you want to customize a default {@code TemplateEngine}, you can configure some property using
610    * mybatis-thymeleaf.properties that encoded by UTF-8. Also, you can change the properties file that will read using
611    * system property (-Dmybatis-thymeleaf.config.file=... -Dmybatis-thymeleaf.config.encoding=...).
612    */
613   static <T extends SqlGeneratorConfig> void applyDefaultProperties(T config) {
614     applyProperties(config, loadDefaultProperties());
615   }
616 
617   /**
618    * Apply properties that read from specified properties file. <br>
619    * you can configure some property using specified properties file that encoded by UTF-8. Also, you can change file
620    * encoding that will read using system property (-Dmybatis-thymeleaf.config.encoding=...).
621    *
622    * @param resourcePath
623    *          A property file resource path
624    */
625   static <T extends SqlGeneratorConfig> void applyResourcePath(T config, String resourcePath) {
626     Properties properties = loadDefaultProperties();
627     properties.putAll(loadProperties(resourcePath));
628     applyProperties(config, properties);
629   }
630 
631   /**
632    * Apply properties from specified properties.
633    *
634    * @param config
635    *          a configuration instance
636    * @param customProperties
637    *          custom configuration properties
638    */
639   static <T extends SqlGeneratorConfig> void applyProperties(T config, Properties customProperties) {
640     Properties properties = loadDefaultProperties();
641     Optional.ofNullable(customProperties).ifPresent(properties::putAll);
642     override(config, properties);
643   }
644 
645   /**
646    * Create new instance using default constructor with specified type.
647    *
648    * @param type
649    *          a target type
650    * @param <T>
651    *          a target type
652    *
653    * @return new instance of target type
654    */
655   static <T> T newInstanceForType(Class<T> type) {
656     try {
657       return type.getConstructor().newInstance();
658     } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
659       throw new IllegalStateException("Cannot create an instance for class: " + type, e);
660     }
661   }
662 
663   private static void override(SqlGeneratorConfig config, Properties properties) {
664     PropertyAccessor standardPropertyAccessor = PropertyAccessor.BuiltIn.STANDARD;
665     try {
666       properties.forEach((key, value) -> {
667         String propertyPath = StringUtils.unCapitalize(StringUtils.capitalizeWords(key, "-").replaceAll("-", ""));
668         try {
669           Object target = config;
670           String propertyName;
671           if (propertyPath.indexOf('.') != -1) {
672             String[] propertyPaths = StringUtils.split(propertyPath, ".");
673             propertyName = propertyPaths[propertyPaths.length - 1];
674             for (String path : Arrays.copyOf(propertyPaths, propertyPaths.length - 1)) {
675               target = standardPropertyAccessor.getPropertyValue(target, path);
676             }
677           } else {
678             propertyName = propertyPath;
679           }
680           Object convertedValue = TYPE_CONVERTERS
681               .getOrDefault(standardPropertyAccessor.getPropertyType(target.getClass(), propertyName), v -> v)
682               .apply(value.toString());
683           standardPropertyAccessor.setPropertyValue(target, propertyName, convertedValue);
684         } catch (IllegalArgumentException e) {
685           throw new IllegalArgumentException(
686               String.format("Detected an invalid property. key='%s' value='%s'", key, value), e);
687         }
688       });
689     } finally {
690       StandardPropertyAccessor.clearCache();
691     }
692   }
693 
694   private static Properties loadDefaultProperties() {
695     return loadProperties(System.getProperty(PropertyKeys.CONFIG_FILE, Defaults.PROPERTIES_FILE));
696   }
697 
698   private static Properties loadProperties(String resourcePath) {
699     Properties properties = new Properties();
700     Optional.ofNullable(ClassLoaderUtils.findResourceAsStream(resourcePath)).ifPresent(in -> {
701       Charset encoding = Optional.ofNullable(System.getProperty(PropertyKeys.CONFIG_ENCODING)).map(Charset::forName)
702           .orElse(StandardCharsets.UTF_8);
703       try (InputStreamReader inReader = new InputStreamReader(in, encoding);
704           BufferedReader bufReader = new BufferedReader(inReader)) {
705         properties.load(bufReader);
706       } catch (IOException e) {
707         throw new IllegalStateException(e);
708       }
709     });
710     return properties;
711   }
712 
713   private static Class<?> toClassForName(String value) {
714     try {
715       return ClassLoaderUtils.loadClass(value.trim());
716     } catch (ClassNotFoundException e) {
717       throw new IllegalStateException(e);
718     }
719   }
720 
721 }