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