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 }