View Javadoc
1   /*
2    *    Copyright 2012-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.velocity;
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.HashSet;
27  import java.util.Map;
28  import java.util.Objects;
29  import java.util.Optional;
30  import java.util.Properties;
31  import java.util.Set;
32  import java.util.StringJoiner;
33  import java.util.function.Consumer;
34  import java.util.function.Function;
35  import java.util.stream.Stream;
36  
37  import org.apache.commons.text.WordUtils;
38  import org.apache.ibatis.io.Resources;
39  import org.apache.ibatis.logging.Log;
40  import org.apache.ibatis.logging.LogFactory;
41  import org.apache.ibatis.reflection.DefaultReflectorFactory;
42  import org.apache.ibatis.reflection.MetaObject;
43  import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
44  import org.apache.ibatis.reflection.property.PropertyTokenizer;
45  import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
46  import org.apache.ibatis.scripting.ScriptingException;
47  import org.apache.velocity.runtime.RuntimeConstants;
48  import org.apache.velocity.runtime.RuntimeInstance;
49  import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
50  
51  /**
52   * Configuration class for {@link Driver}.
53   *
54   * @author Kazuki Shimizu
55   *
56   * @since 2.1.0
57   */
58  public class VelocityLanguageDriverConfig {
59  
60    private static final String PROPERTY_KEY_CONFIG_FILE = "mybatis-velocity.config.file";
61    private static final String PROPERTY_KEY_CONFIG_ENCODING = "mybatis-velocity.config.encoding";
62    private static final String DEFAULT_PROPERTIES_FILE = "mybatis-velocity.properties";
63    private static final String PROPERTY_KEY_ADDITIONAL_CONTEXT_ATTRIBUTE = "additional.context.attributes";
64    private static final String[] BUILT_IN_DIRECTIVES = { TrimDirective.class.getName(), WhereDirective.class.getName(),
65        SetDirective.class.getName(), InDirective.class.getName(), RepeatDirective.class.getName() };
66  
67    private static final Map<Class<?>, Function<String, Object>> TYPE_CONVERTERS;
68    static {
69      Map<Class<?>, Function<String, Object>> converters = new HashMap<>();
70      converters.put(String.class, String::trim);
71      converters.put(Charset.class, v -> Charset.forName(v.trim()));
72      converters.put(String[].class, v -> Stream.of(v.split(",")).map(String::trim).toArray(String[]::new));
73      converters.put(Object.class, v -> v);
74      TYPE_CONVERTERS = Collections.unmodifiableMap(converters);
75    }
76  
77    private static final Log log = LogFactory.getLog(VelocityLanguageDriverConfig.class);
78  
79    /**
80     * The Velocity settings.
81     */
82    private final Map<String, String> velocitySettings = new HashMap<>();
83    {
84      velocitySettings.put(RuntimeConstants.RESOURCE_LOADERS, "class");
85      velocitySettings.put(RuntimeConstants.RESOURCE_LOADER + ".class.class", ClasspathResourceLoader.class.getName());
86    }
87  
88    /**
89     * The base directory for reading template resources.
90     */
91    private String[] userDirectives = {};
92  
93    /**
94     * The additional context attribute.
95     */
96    private final Map<String, String> additionalContextAttributes = new HashMap<>();
97  
98    /**
99     * Get Velocity settings.
100    *
101    * @return Velocity settings
102    */
103   public Map<String, String> getVelocitySettings() {
104     return velocitySettings;
105   }
106 
107   /**
108    * Get user define directives.
109    *
110    * @return user define directives.
111    *
112    * @deprecated Recommend to use the 'velocity-settings.runtime.custom_directives' or 'runtime.custom_directives'
113    *             because this method defined for keeping backward compatibility (There is possibility that this method
114    *             removed at a future version)
115    */
116   @Deprecated
117   public String[] getUserdirective() {
118     return userDirectives;
119   }
120 
121   /**
122    * Set user define directives.
123    *
124    * @param userDirectives
125    *          user define directives
126    *
127    * @deprecated Recommend to use the 'velocity-settings.runtime.custom_directives' or 'runtime.custom_directives'
128    *             because this method defined for keeping backward compatibility (There is possibility that this method
129    *             removed at a future version)
130    */
131   @Deprecated
132   public void setUserdirective(String... userDirectives) {
133     log.warn(
134         "The 'userdirective' has been deprecated since 2.1.0. Please use the 'velocity-settings.runtime.custom_directives' or 'runtime.custom_directives'.");
135     this.userDirectives = userDirectives;
136   }
137 
138   /**
139    * Get additional context attributes.
140    *
141    * @return additional context attributes
142    */
143   public Map<String, String> getAdditionalContextAttributes() {
144     return additionalContextAttributes;
145   }
146 
147   /**
148    * Generate a custom directives string.
149    *
150    * @return a custom directives string
151    */
152   public String generateCustomDirectivesString() {
153     StringJoiner customDirectivesJoiner = new StringJoiner(",");
154     Optional.ofNullable(velocitySettings.get(RuntimeConstants.CUSTOM_DIRECTIVES))
155         .ifPresent(customDirectivesJoiner::add);
156     Stream.of(userDirectives).forEach(customDirectivesJoiner::add);
157     Stream.of(BUILT_IN_DIRECTIVES).forEach(customDirectivesJoiner::add);
158     return customDirectivesJoiner.toString();
159   }
160 
161   /**
162    * Create an instance from default properties file. <br>
163    * If you want to customize a default {@link RuntimeInstance}, you can configure some property using
164    * mybatis-velocity.properties that encoded by UTF-8. Also, you can change the properties file that will read using
165    * system property (-Dmybatis-velocity.config.file=... -Dmybatis-velocity.config.encoding=...). <br>
166    * Supported properties are as follows:
167    * <table border="1">
168    * <caption>Supported properties</caption>
169    * <tr>
170    * <th>Property Key</th>
171    * <th>Description</th>
172    * <th>Default</th>
173    * </tr>
174    * <tr>
175    * <th colspan="3">Directive configuration</th>
176    * </tr>
177    * <tr>
178    * <td>userdirective</td>
179    * <td>The user defined directives (Recommend to use the 'velocity-settings.runtime.custom_directives' property
180    * because this property defined for keeping backward compatibility)</td>
181    * <td>None(empty)</td>
182    * </tr>
183    * <tr>
184    * <th colspan="3">Additional context attribute configuration</th>
185    * </tr>
186    * <tr>
187    * <td>additional.context.attributes</td>
188    * <td>The user defined additional context attribute values(Recommend to use the
189    * 'additional-context-attributes.{name}' because this property defined for keeping backward compatibility)</td>
190    * <td>None(empty)</td>
191    * </tr>
192    * <tr>
193    * <td>additional-context-attributes.{name}</td>
194    * <td>The user defined additional context attributes value(FQCN)</td>
195    * <td>-</td>
196    * </tr>
197    * <tr>
198    * <th colspan="3">Velocity settings configuration</th>
199    * </tr>
200    * <tr>
201    * <td>velocity-settings.{name}</td>
202    * <td>The settings of Velocity's {@link RuntimeInstance#setProperty(String, Object)}</td>
203    * <td>-</td>
204    * </tr>
205    * <tr>
206    * <td>{name}</td>
207    * <td>The settings of Velocity's {@link RuntimeInstance#setProperty(String, Object)} (Recommend to use the
208    * 'velocity-settings.{name}' because this property defined for keeping backward compatibility)</td>
209    * <td>-</td>
210    * </tr>
211    * </table>
212    *
213    * @return a configuration instance
214    */
215   public static VelocityLanguageDriverConfig newInstance() {
216     return newInstance(loadDefaultProperties());
217   }
218 
219   /**
220    * Create an instance from specified properties.
221    *
222    * @param customProperties
223    *          custom configuration properties
224    *
225    * @return a configuration instance
226    *
227    * @see #newInstance()
228    */
229   public static VelocityLanguageDriverConfig newInstance(Properties customProperties) {
230     VelocityLanguageDriverConfig config = new VelocityLanguageDriverConfig();
231     Properties properties = loadDefaultProperties();
232     Optional.ofNullable(customProperties).ifPresent(properties::putAll);
233     override(config, properties);
234     configureVelocitySettings(config, properties);
235     return config;
236   }
237 
238   /**
239    * Create an instance using specified customizer and override using a default properties file.
240    *
241    * @param customizer
242    *          baseline customizer
243    *
244    * @return a configuration instance
245    *
246    * @see #newInstance()
247    */
248   public static VelocityLanguageDriverConfig newInstance(Consumer<VelocityLanguageDriverConfig> customizer) {
249     VelocityLanguageDriverConfig config = new VelocityLanguageDriverConfig();
250     Properties properties = loadDefaultProperties();
251     customizer.accept(config);
252     override(config, properties);
253     configureVelocitySettings(config, properties);
254     return config;
255   }
256 
257   private static void override(VelocityLanguageDriverConfig config, Properties properties) {
258     enableLegacyAdditionalContextAttributes(properties);
259     MetaObject metaObject = MetaObject.forObject(config, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(),
260         new DefaultReflectorFactory());
261     Set<Object> consumedKeys = new HashSet<>();
262     properties.forEach((key, value) -> {
263       String propertyPath = WordUtils
264           .uncapitalize(WordUtils.capitalize(Objects.toString(key), '-').replaceAll("-", ""));
265       if (metaObject.hasSetter(propertyPath)) {
266         PropertyTokenizer pt = new PropertyTokenizer(propertyPath);
267         if (Map.class.isAssignableFrom(metaObject.getGetterType(pt.getName()))) {
268           @SuppressWarnings("unchecked")
269           Map<String, Object> map = (Map<String, Object>) metaObject.getValue(pt.getName());
270           map.put(pt.getChildren(), value);
271         } else {
272           Optional.ofNullable(value).ifPresent(v -> {
273             Object convertedValue = TYPE_CONVERTERS.get(metaObject.getSetterType(propertyPath)).apply(value.toString());
274             metaObject.setValue(propertyPath, convertedValue);
275           });
276         }
277         consumedKeys.add(key);
278       }
279     });
280     consumedKeys.forEach(properties::remove);
281   }
282 
283   private static void enableLegacyAdditionalContextAttributes(Properties properties) {
284     String additionalContextAttributes = properties.getProperty(PROPERTY_KEY_ADDITIONAL_CONTEXT_ATTRIBUTE);
285     if (Objects.nonNull(additionalContextAttributes)) {
286       log.warn(String.format(
287           "The '%s' has been deprecated since 2.1.0. Please use the 'additionalContextAttributes.{name}={value}'.",
288           PROPERTY_KEY_ADDITIONAL_CONTEXT_ATTRIBUTE));
289       Stream.of(additionalContextAttributes.split(",")).forEach(pair -> {
290         String[] keyValue = pair.split(":");
291         if (keyValue.length != 2) {
292           throw new ScriptingException("Invalid additional context property '" + pair + "' on '"
293               + PROPERTY_KEY_ADDITIONAL_CONTEXT_ATTRIBUTE + "'. Must be specify by 'key:value' format.");
294         }
295         properties.setProperty("additional-context-attributes." + keyValue[0].trim(), keyValue[1].trim());
296       });
297       properties.remove(PROPERTY_KEY_ADDITIONAL_CONTEXT_ATTRIBUTE);
298     }
299   }
300 
301   private static void configureVelocitySettings(VelocityLanguageDriverConfig config, Properties properties) {
302     properties.forEach((name, value) -> config.getVelocitySettings().put((String) name, (String) value));
303   }
304 
305   private static Properties loadDefaultProperties() {
306     return loadProperties(System.getProperty(PROPERTY_KEY_CONFIG_FILE, DEFAULT_PROPERTIES_FILE));
307   }
308 
309   private static Properties loadProperties(String resourcePath) {
310     Properties properties = new Properties();
311     InputStream in;
312     try {
313       in = Resources.getResourceAsStream(resourcePath);
314     } catch (IOException e) {
315       in = null;
316     }
317     if (in != null) {
318       Charset encoding = Optional.ofNullable(System.getProperty(PROPERTY_KEY_CONFIG_ENCODING)).map(Charset::forName)
319           .orElse(StandardCharsets.UTF_8);
320       try (InputStreamReader inReader = new InputStreamReader(in, encoding);
321           BufferedReader bufReader = new BufferedReader(inReader)) {
322         properties.load(bufReader);
323       } catch (IOException e) {
324         throw new IllegalStateException(e);
325       }
326     }
327     return properties;
328   }
329 
330 }