VelocityLanguageDriverConfig.java
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mybatis.scripting.velocity;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import org.apache.commons.text.WordUtils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.scripting.ScriptingException;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.RuntimeInstance;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
/**
* Configuration class for {@link Driver}.
*
* @author Kazuki Shimizu
*
* @since 2.1.0
*/
public class VelocityLanguageDriverConfig {
private static final String PROPERTY_KEY_CONFIG_FILE = "mybatis-velocity.config.file";
private static final String PROPERTY_KEY_CONFIG_ENCODING = "mybatis-velocity.config.encoding";
private static final String DEFAULT_PROPERTIES_FILE = "mybatis-velocity.properties";
private static final String PROPERTY_KEY_ADDITIONAL_CONTEXT_ATTRIBUTE = "additional.context.attributes";
private static final String[] BUILT_IN_DIRECTIVES = { TrimDirective.class.getName(), WhereDirective.class.getName(),
SetDirective.class.getName(), InDirective.class.getName(), RepeatDirective.class.getName() };
private static final Map<Class<?>, Function<String, Object>> TYPE_CONVERTERS;
static {
Map<Class<?>, Function<String, Object>> converters = new HashMap<>();
converters.put(String.class, String::trim);
converters.put(Charset.class, v -> Charset.forName(v.trim()));
converters.put(String[].class, v -> Stream.of(v.split(",")).map(String::trim).toArray(String[]::new));
converters.put(Object.class, v -> v);
TYPE_CONVERTERS = Collections.unmodifiableMap(converters);
}
private static final Log log = LogFactory.getLog(VelocityLanguageDriverConfig.class);
/**
* The Velocity settings.
*/
private final Map<String, String> velocitySettings = new HashMap<>();
{
velocitySettings.put(RuntimeConstants.RESOURCE_LOADERS, "class");
velocitySettings.put(RuntimeConstants.RESOURCE_LOADER + ".class.class", ClasspathResourceLoader.class.getName());
}
/**
* The base directory for reading template resources.
*/
private String[] userDirectives = {};
/**
* The additional context attribute.
*/
private final Map<String, String> additionalContextAttributes = new HashMap<>();
/**
* Get Velocity settings.
*
* @return Velocity settings
*/
public Map<String, String> getVelocitySettings() {
return velocitySettings;
}
/**
* Get user define directives.
*
* @return user define directives.
*
* @deprecated Recommend to use the 'velocity-settings.runtime.custom_directives' or 'runtime.custom_directives'
* because this method defined for keeping backward compatibility (There is possibility that this method
* removed at a future version)
*/
@Deprecated
public String[] getUserdirective() {
return userDirectives;
}
/**
* Set user define directives.
*
* @param userDirectives
* user define directives
*
* @deprecated Recommend to use the 'velocity-settings.runtime.custom_directives' or 'runtime.custom_directives'
* because this method defined for keeping backward compatibility (There is possibility that this method
* removed at a future version)
*/
@Deprecated
public void setUserdirective(String... userDirectives) {
log.warn(
"The 'userdirective' has been deprecated since 2.1.0. Please use the 'velocity-settings.runtime.custom_directives' or 'runtime.custom_directives'.");
this.userDirectives = userDirectives;
}
/**
* Get additional context attributes.
*
* @return additional context attributes
*/
public Map<String, String> getAdditionalContextAttributes() {
return additionalContextAttributes;
}
/**
* Generate a custom directives string.
*
* @return a custom directives string
*/
public String generateCustomDirectivesString() {
StringJoiner customDirectivesJoiner = new StringJoiner(",");
Optional.ofNullable(velocitySettings.get(RuntimeConstants.CUSTOM_DIRECTIVES))
.ifPresent(customDirectivesJoiner::add);
Stream.of(userDirectives).forEach(customDirectivesJoiner::add);
Stream.of(BUILT_IN_DIRECTIVES).forEach(customDirectivesJoiner::add);
return customDirectivesJoiner.toString();
}
/**
* Create an instance from default properties file. <br>
* If you want to customize a default {@link RuntimeInstance}, you can configure some property using
* mybatis-velocity.properties that encoded by UTF-8. Also, you can change the properties file that will read using
* system property (-Dmybatis-velocity.config.file=... -Dmybatis-velocity.config.encoding=...). <br>
* Supported properties are as follows:
* <table border="1">
* <caption>Supported properties</caption>
* <tr>
* <th>Property Key</th>
* <th>Description</th>
* <th>Default</th>
* </tr>
* <tr>
* <th colspan="3">Directive configuration</th>
* </tr>
* <tr>
* <td>userdirective</td>
* <td>The user defined directives (Recommend to use the 'velocity-settings.runtime.custom_directives' property
* because this property defined for keeping backward compatibility)</td>
* <td>None(empty)</td>
* </tr>
* <tr>
* <th colspan="3">Additional context attribute configuration</th>
* </tr>
* <tr>
* <td>additional.context.attributes</td>
* <td>The user defined additional context attribute values(Recommend to use the
* 'additional-context-attributes.{name}' because this property defined for keeping backward compatibility)</td>
* <td>None(empty)</td>
* </tr>
* <tr>
* <td>additional-context-attributes.{name}</td>
* <td>The user defined additional context attributes value(FQCN)</td>
* <td>-</td>
* </tr>
* <tr>
* <th colspan="3">Velocity settings configuration</th>
* </tr>
* <tr>
* <td>velocity-settings.{name}</td>
* <td>The settings of Velocity's {@link RuntimeInstance#setProperty(String, Object)}</td>
* <td>-</td>
* </tr>
* <tr>
* <td>{name}</td>
* <td>The settings of Velocity's {@link RuntimeInstance#setProperty(String, Object)} (Recommend to use the
* 'velocity-settings.{name}' because this property defined for keeping backward compatibility)</td>
* <td>-</td>
* </tr>
* </table>
*
* @return a configuration instance
*/
public static VelocityLanguageDriverConfig newInstance() {
return newInstance(loadDefaultProperties());
}
/**
* Create an instance from specified properties.
*
* @param customProperties
* custom configuration properties
*
* @return a configuration instance
*
* @see #newInstance()
*/
public static VelocityLanguageDriverConfig newInstance(Properties customProperties) {
VelocityLanguageDriverConfig config = new VelocityLanguageDriverConfig();
Properties properties = loadDefaultProperties();
Optional.ofNullable(customProperties).ifPresent(properties::putAll);
override(config, properties);
configureVelocitySettings(config, properties);
return config;
}
/**
* Create an instance using specified customizer and override using a default properties file.
*
* @param customizer
* baseline customizer
*
* @return a configuration instance
*
* @see #newInstance()
*/
public static VelocityLanguageDriverConfig newInstance(Consumer<VelocityLanguageDriverConfig> customizer) {
VelocityLanguageDriverConfig config = new VelocityLanguageDriverConfig();
Properties properties = loadDefaultProperties();
customizer.accept(config);
override(config, properties);
configureVelocitySettings(config, properties);
return config;
}
private static void override(VelocityLanguageDriverConfig config, Properties properties) {
enableLegacyAdditionalContextAttributes(properties);
MetaObject metaObject = MetaObject.forObject(config, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(),
new DefaultReflectorFactory());
Set<Object> consumedKeys = new HashSet<>();
properties.forEach((key, value) -> {
String propertyPath = WordUtils
.uncapitalize(WordUtils.capitalize(Objects.toString(key), '-').replaceAll("-", ""));
if (metaObject.hasSetter(propertyPath)) {
PropertyTokenizer pt = new PropertyTokenizer(propertyPath);
if (Map.class.isAssignableFrom(metaObject.getGetterType(pt.getName()))) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) metaObject.getValue(pt.getName());
map.put(pt.getChildren(), value);
} else {
Optional.ofNullable(value).ifPresent(v -> {
Object convertedValue = TYPE_CONVERTERS.get(metaObject.getSetterType(propertyPath)).apply(value.toString());
metaObject.setValue(propertyPath, convertedValue);
});
}
consumedKeys.add(key);
}
});
consumedKeys.forEach(properties::remove);
}
private static void enableLegacyAdditionalContextAttributes(Properties properties) {
String additionalContextAttributes = properties.getProperty(PROPERTY_KEY_ADDITIONAL_CONTEXT_ATTRIBUTE);
if (Objects.nonNull(additionalContextAttributes)) {
log.warn(String.format(
"The '%s' has been deprecated since 2.1.0. Please use the 'additionalContextAttributes.{name}={value}'.",
PROPERTY_KEY_ADDITIONAL_CONTEXT_ATTRIBUTE));
Stream.of(additionalContextAttributes.split(",")).forEach(pair -> {
String[] keyValue = pair.split(":");
if (keyValue.length != 2) {
throw new ScriptingException("Invalid additional context property '" + pair + "' on '"
+ PROPERTY_KEY_ADDITIONAL_CONTEXT_ATTRIBUTE + "'. Must be specify by 'key:value' format.");
}
properties.setProperty("additional-context-attributes." + keyValue[0].trim(), keyValue[1].trim());
});
properties.remove(PROPERTY_KEY_ADDITIONAL_CONTEXT_ATTRIBUTE);
}
}
private static void configureVelocitySettings(VelocityLanguageDriverConfig config, Properties properties) {
properties.forEach((name, value) -> config.getVelocitySettings().put((String) name, (String) value));
}
private static Properties loadDefaultProperties() {
return loadProperties(System.getProperty(PROPERTY_KEY_CONFIG_FILE, DEFAULT_PROPERTIES_FILE));
}
private static Properties loadProperties(String resourcePath) {
Properties properties = new Properties();
InputStream in;
try {
in = Resources.getResourceAsStream(resourcePath);
} catch (IOException e) {
in = null;
}
if (in != null) {
Charset encoding = Optional.ofNullable(System.getProperty(PROPERTY_KEY_CONFIG_ENCODING)).map(Charset::forName)
.orElse(StandardCharsets.UTF_8);
try (InputStreamReader inReader = new InputStreamReader(in, encoding);
BufferedReader bufReader = new BufferedReader(inReader)) {
properties.load(bufReader);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
return properties;
}
}