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.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 }