View Javadoc
1   /*
2    *    Copyright 2015-2023 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.freemarker;
17  
18  import java.io.BufferedReader;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.InputStreamReader;
22  import java.nio.charset.Charset;
23  import java.nio.charset.StandardCharsets;
24  import java.util.Collections;
25  import java.util.HashMap;
26  import java.util.Map;
27  import java.util.Objects;
28  import java.util.Optional;
29  import java.util.Properties;
30  import java.util.function.Consumer;
31  import java.util.function.Function;
32  
33  import org.apache.commons.text.WordUtils;
34  import org.apache.ibatis.io.Resources;
35  import org.apache.ibatis.logging.Log;
36  import org.apache.ibatis.logging.LogFactory;
37  import org.apache.ibatis.reflection.DefaultReflectorFactory;
38  import org.apache.ibatis.reflection.MetaObject;
39  import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
40  import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
41  
42  /**
43   * Configuration class for {@link FreeMarkerLanguageDriver}.
44   *
45   * @author Kazuki Shimizu
46   *
47   * @since 1.2.0
48   */
49  public class FreeMarkerLanguageDriverConfig {
50    private static final String PROPERTY_KEY_CONFIG_FILE = "mybatis-freemarker.config.file";
51    private static final String PROPERTY_KEY_CONFIG_ENCODING = "mybatis-freemarker.config.encoding";
52    private static final String DEFAULT_PROPERTIES_FILE = "mybatis-freemarker.properties";
53    private static final Map<Class<?>, Function<String, Object>> TYPE_CONVERTERS;
54  
55    static {
56      Map<Class<?>, Function<String, Object>> converters = new HashMap<>();
57      converters.put(String.class, String::trim);
58      converters.put(boolean.class, v -> Boolean.valueOf(v.trim()));
59      converters.put(Object.class, v -> v);
60      TYPE_CONVERTERS = Collections.unmodifiableMap(converters);
61    }
62  
63    private static final Log log = LogFactory.getLog(FreeMarkerLanguageDriverConfig.class);
64  
65    /**
66     * The configuration properties.
67     */
68    private final Map<String, String> freemarkerSettings = new HashMap<>();
69  
70    /**
71     * Template file configuration.
72     */
73    private final TemplateFileConfig templateFile = new TemplateFileConfig();
74  
75    /**
76     * Get FreeMarker settings.
77     *
78     * @return FreeMarker settings
79     */
80    public Map<String, String> getFreemarkerSettings() {
81      return freemarkerSettings;
82    }
83  
84    /**
85     * Get a base directory for reading template resources.
86     * <p>
87     * Default is none (just under classpath).
88     * </p>
89     *
90     * @return a base directory for reading template resources
91     *
92     * @deprecated Recommend to use the {@link TemplateFileConfig#getBaseDir()}} because this method defined for keeping
93     *             backward compatibility (There is possibility that this method removed at a future version)
94     */
95    @Deprecated
96    public String getBasePackage() {
97      return templateFile.getBaseDir();
98    }
99  
100   /**
101    * Set a base directory for reading template resources.
102    *
103    * @param basePackage
104    *          a base directory for reading template resources
105    *
106    * @deprecated Recommend to use the {@link TemplateFileConfig#setBaseDir(String)} because this method defined for
107    *             keeping backward compatibility (There is possibility that this method removed at a future version)
108    */
109   @Deprecated
110   public void setBasePackage(String basePackage) {
111     log.warn("The 'basePackage' has been deprecated since 1.2.0. Please use the 'templateFile.baseDir'.");
112     templateFile.setBaseDir(basePackage);
113   }
114 
115   /**
116    * Get a template file configuration.
117    *
118    * @return a template file configuration
119    */
120   public TemplateFileConfig getTemplateFile() {
121     return templateFile;
122   }
123 
124   /**
125    * Template file configuration.
126    */
127   public static class TemplateFileConfig {
128 
129     /**
130      * The base directory for reading template resources.
131      */
132     private String baseDir = "";
133 
134     /**
135      * The template file path provider configuration.
136      */
137     private final PathProviderConfig pathProvider = new PathProviderConfig();
138 
139     /**
140      * Get the base directory for reading template resource file.
141      * <p>
142      * Default is {@code ""}(none).
143      * </p>
144      *
145      * @return the base directory for reading template resource file
146      */
147     public String getBaseDir() {
148       return baseDir;
149     }
150 
151     /**
152      * Set the base directory for reading template resource file.
153      *
154      * @param baseDir
155      *          the base directory for reading template resource file
156      */
157     public void setBaseDir(String baseDir) {
158       this.baseDir = baseDir;
159     }
160 
161     /**
162      * Get the template file path provider configuration.
163      *
164      * @return the template file path provider configuration
165      */
166     public PathProviderConfig getPathProvider() {
167       return pathProvider;
168     }
169 
170     /**
171      * The template file path provider configuration.
172      */
173     public static class PathProviderConfig {
174 
175       /**
176        * The prefix for adding to template file path.
177        */
178       private String prefix = "";
179 
180       /**
181        * Whether includes package path part.
182        */
183       private boolean includesPackagePath = true;
184 
185       /**
186        * Whether separate directory per mapper.
187        */
188       private boolean separateDirectoryPerMapper = true;
189 
190       /**
191        * Whether includes mapper name into file name when separate directory per mapper.
192        */
193       private boolean includesMapperNameWhenSeparateDirectory = true;
194 
195       /**
196        * Whether cache a resolved template file path.
197        */
198       private boolean cacheEnabled = true;
199 
200       /**
201        * Get a prefix for adding to template file path.
202        * <p>
203        * Default is {@code ""}.
204        * </p>
205        *
206        * @return a prefix for adding to template file path
207        */
208       public String getPrefix() {
209         return prefix;
210       }
211 
212       /**
213        * Set the prefix for adding to template file path.
214        *
215        * @param prefix
216        *          The prefix for adding to template file path
217        */
218       public void setPrefix(String prefix) {
219         this.prefix = prefix;
220       }
221 
222       /**
223        * Get whether includes package path part.
224        * <p>
225        * Default is {@code true}.
226        * </p>
227        *
228        * @return If includes package path, return {@code true}
229        */
230       public boolean isIncludesPackagePath() {
231         return includesPackagePath;
232       }
233 
234       /**
235        * Set whether includes package path part.
236        *
237        * @param includesPackagePath
238        *          If want to includes, set {@code true}
239        */
240       public void setIncludesPackagePath(boolean includesPackagePath) {
241         this.includesPackagePath = includesPackagePath;
242       }
243 
244       /**
245        * Get whether separate directory per mapper.
246        *
247        * @return If separate directory per mapper, return {@code true}
248        */
249       public boolean isSeparateDirectoryPerMapper() {
250         return separateDirectoryPerMapper;
251       }
252 
253       /**
254        * Set whether separate directory per mapper.
255        * <p>
256        * Default is {@code true}.
257        * </p>
258        *
259        * @param separateDirectoryPerMapper
260        *          If want to separate directory, set {@code true}
261        */
262       public void setSeparateDirectoryPerMapper(boolean separateDirectoryPerMapper) {
263         this.separateDirectoryPerMapper = separateDirectoryPerMapper;
264       }
265 
266       /**
267        * Get whether includes mapper name into file name when separate directory per mapper.
268        * <p>
269        * Default is {@code true}.
270        * </p>
271        *
272        * @return If includes mapper name, return {@code true}
273        */
274       public boolean isIncludesMapperNameWhenSeparateDirectory() {
275         return includesMapperNameWhenSeparateDirectory;
276       }
277 
278       /**
279        * Set whether includes mapper name into file name when separate directory per mapper.
280        * <p>
281        * Default is {@code true}.
282        * </p>
283        *
284        * @param includesMapperNameWhenSeparateDirectory
285        *          If want to includes, set {@code true}
286        */
287       public void setIncludesMapperNameWhenSeparateDirectory(boolean includesMapperNameWhenSeparateDirectory) {
288         this.includesMapperNameWhenSeparateDirectory = includesMapperNameWhenSeparateDirectory;
289       }
290 
291       /**
292        * Get whether cache a resolved template file path.
293        * <p>
294        * Default is {@code true}.
295        * </p>
296        *
297        * @return If cache a resolved template file path, return {@code true}
298        */
299       public boolean isCacheEnabled() {
300         return cacheEnabled;
301       }
302 
303       /**
304        * Set whether cache a resolved template file path.
305        *
306        * @param cacheEnabled
307        *          If want to cache, set {@code true}
308        */
309       public void setCacheEnabled(boolean cacheEnabled) {
310         this.cacheEnabled = cacheEnabled;
311       }
312 
313     }
314 
315   }
316 
317   /**
318    * Create an instance from default properties file. <br>
319    * If you want to customize a default {@code TemplateEngine}, you can configure some property using
320    * mybatis-freemarker.properties that encoded by UTF-8. Also, you can change the properties file that will read using
321    * system property (-Dmybatis-freemarker.config.file=... -Dmybatis-freemarker.config.encoding=...). <br>
322    * Supported properties are as follows:
323    * <table border="1">
324    * <caption>Supported properties</caption>
325    * <tr>
326    * <th>Property Key</th>
327    * <th>Description</th>
328    * <th>Default</th>
329    * </tr>
330    * <tr>
331    * <th colspan="3">General configuration</th>
332    * </tr>
333    * <tr>
334    * <td>base-package</td>
335    * <td>The base directory for reading template resources</td>
336    * <td>None(just under classpath)</td>
337    * </tr>
338    * <tr>
339    * <td>freemarker-settings.*</td>
340    * <td>The settings of freemarker {@link freemarker.core.Configurable#setSetting(String, String)}).</td>
341    * <td>-</td>
342    * </tr>
343    * </table>
344    *
345    * @return a configuration instance
346    */
347   public static FreeMarkerLanguageDriverConfig newInstance() {
348     return newInstance(loadDefaultProperties());
349   }
350 
351   /**
352    * Create an instance from specified properties.
353    *
354    * @param customProperties
355    *          custom configuration properties
356    *
357    * @return a configuration instance
358    *
359    * @see #newInstance()
360    */
361   public static FreeMarkerLanguageDriverConfig newInstance(Properties customProperties) {
362     FreeMarkerLanguageDriverConfig config = new FreeMarkerLanguageDriverConfig();
363     Properties properties = loadDefaultProperties();
364     Optional.ofNullable(customProperties).ifPresent(properties::putAll);
365     override(config, properties);
366     return config;
367   }
368 
369   /**
370    * Create an instance using specified customizer and override using a default properties file.
371    *
372    * @param customizer
373    *          baseline customizer
374    *
375    * @return a configuration instance
376    *
377    * @see #newInstance()
378    */
379   public static FreeMarkerLanguageDriverConfig newInstance(Consumer<FreeMarkerLanguageDriverConfig> customizer) {
380     FreeMarkerLanguageDriverConfig config = new FreeMarkerLanguageDriverConfig();
381     customizer.accept(config);
382     override(config, loadDefaultProperties());
383     return config;
384   }
385 
386   private static void override(FreeMarkerLanguageDriverConfig config, Properties properties) {
387     MetaObject metaObject = MetaObject.forObject(config, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(),
388         new DefaultReflectorFactory());
389     properties.forEach((key, value) -> {
390       String propertyPath = WordUtils
391           .uncapitalize(WordUtils.capitalize(Objects.toString(key), '-').replaceAll("-", ""));
392       Optional.ofNullable(value).ifPresent(v -> {
393         Object convertedValue = TYPE_CONVERTERS.get(metaObject.getSetterType(propertyPath)).apply(value.toString());
394         metaObject.setValue(propertyPath, convertedValue);
395       });
396     });
397   }
398 
399   private static Properties loadDefaultProperties() {
400     return loadProperties(System.getProperty(PROPERTY_KEY_CONFIG_FILE, DEFAULT_PROPERTIES_FILE));
401   }
402 
403   private static Properties loadProperties(String resourcePath) {
404     Properties properties = new Properties();
405     InputStream in;
406     try {
407       in = Resources.getResourceAsStream(resourcePath);
408     } catch (IOException e) {
409       in = null;
410     }
411     if (in != null) {
412       Charset encoding = Optional.ofNullable(System.getProperty(PROPERTY_KEY_CONFIG_ENCODING)).map(Charset::forName)
413           .orElse(StandardCharsets.UTF_8);
414       try (InputStreamReader inReader = new InputStreamReader(in, encoding);
415           BufferedReader bufReader = new BufferedReader(inReader)) {
416         properties.load(bufReader);
417       } catch (IOException e) {
418         throw new IllegalStateException(e);
419       }
420     }
421     return properties;
422   }
423 
424 }