1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
53
54
55
56
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
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
90
91 private String[] userDirectives = {};
92
93
94
95
96 private final Map<String, String> additionalContextAttributes = new HashMap<>();
97
98
99
100
101
102
103 public Map<String, String> getVelocitySettings() {
104 return velocitySettings;
105 }
106
107
108
109
110
111
112
113
114
115
116 @Deprecated
117 public String[] getUserdirective() {
118 return userDirectives;
119 }
120
121
122
123
124
125
126
127
128
129
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
140
141
142
143 public Map<String, String> getAdditionalContextAttributes() {
144 return additionalContextAttributes;
145 }
146
147
148
149
150
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215 public static VelocityLanguageDriverConfig newInstance() {
216 return newInstance(loadDefaultProperties());
217 }
218
219
220
221
222
223
224
225
226
227
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
240
241
242
243
244
245
246
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 }