FreeMarkerLanguageDriverConfig.java

/*
 *    Copyright 2015-2023 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.freemarker;

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.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.function.Consumer;
import java.util.function.Function;

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.wrapper.DefaultObjectWrapperFactory;

/**
 * Configuration class for {@link FreeMarkerLanguageDriver}.
 *
 * @author Kazuki Shimizu
 *
 * @since 1.2.0
 */
public class FreeMarkerLanguageDriverConfig {
  private static final String PROPERTY_KEY_CONFIG_FILE = "mybatis-freemarker.config.file";
  private static final String PROPERTY_KEY_CONFIG_ENCODING = "mybatis-freemarker.config.encoding";
  private static final String DEFAULT_PROPERTIES_FILE = "mybatis-freemarker.properties";
  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(boolean.class, v -> Boolean.valueOf(v.trim()));
    converters.put(Object.class, v -> v);
    TYPE_CONVERTERS = Collections.unmodifiableMap(converters);
  }

  private static final Log log = LogFactory.getLog(FreeMarkerLanguageDriverConfig.class);

  /**
   * The configuration properties.
   */
  private final Map<String, String> freemarkerSettings = new HashMap<>();

  /**
   * Template file configuration.
   */
  private final TemplateFileConfig templateFile = new TemplateFileConfig();

  /**
   * Get FreeMarker settings.
   *
   * @return FreeMarker settings
   */
  public Map<String, String> getFreemarkerSettings() {
    return freemarkerSettings;
  }

  /**
   * Get a base directory for reading template resources.
   * <p>
   * Default is none (just under classpath).
   * </p>
   *
   * @return a base directory for reading template resources
   *
   * @deprecated Recommend to use the {@link TemplateFileConfig#getBaseDir()}} because this method defined for keeping
   *             backward compatibility (There is possibility that this method removed at a future version)
   */
  @Deprecated
  public String getBasePackage() {
    return templateFile.getBaseDir();
  }

  /**
   * Set a base directory for reading template resources.
   *
   * @param basePackage
   *          a base directory for reading template resources
   *
   * @deprecated Recommend to use the {@link TemplateFileConfig#setBaseDir(String)} because this method defined for
   *             keeping backward compatibility (There is possibility that this method removed at a future version)
   */
  @Deprecated
  public void setBasePackage(String basePackage) {
    log.warn("The 'basePackage' has been deprecated since 1.2.0. Please use the 'templateFile.baseDir'.");
    templateFile.setBaseDir(basePackage);
  }

  /**
   * Get a template file configuration.
   *
   * @return a template file configuration
   */
  public TemplateFileConfig getTemplateFile() {
    return templateFile;
  }

  /**
   * Template file configuration.
   */
  public static class TemplateFileConfig {

    /**
     * The base directory for reading template resources.
     */
    private String baseDir = "";

    /**
     * The template file path provider configuration.
     */
    private final PathProviderConfig pathProvider = new PathProviderConfig();

    /**
     * Get the base directory for reading template resource file.
     * <p>
     * Default is {@code ""}(none).
     * </p>
     *
     * @return the base directory for reading template resource file
     */
    public String getBaseDir() {
      return baseDir;
    }

    /**
     * Set the base directory for reading template resource file.
     *
     * @param baseDir
     *          the base directory for reading template resource file
     */
    public void setBaseDir(String baseDir) {
      this.baseDir = baseDir;
    }

    /**
     * Get the template file path provider configuration.
     *
     * @return the template file path provider configuration
     */
    public PathProviderConfig getPathProvider() {
      return pathProvider;
    }

    /**
     * The template file path provider configuration.
     */
    public static class PathProviderConfig {

      /**
       * The prefix for adding to template file path.
       */
      private String prefix = "";

      /**
       * Whether includes package path part.
       */
      private boolean includesPackagePath = true;

      /**
       * Whether separate directory per mapper.
       */
      private boolean separateDirectoryPerMapper = true;

      /**
       * Whether includes mapper name into file name when separate directory per mapper.
       */
      private boolean includesMapperNameWhenSeparateDirectory = true;

      /**
       * Whether cache a resolved template file path.
       */
      private boolean cacheEnabled = true;

      /**
       * Get a prefix for adding to template file path.
       * <p>
       * Default is {@code ""}.
       * </p>
       *
       * @return a prefix for adding to template file path
       */
      public String getPrefix() {
        return prefix;
      }

      /**
       * Set the prefix for adding to template file path.
       *
       * @param prefix
       *          The prefix for adding to template file path
       */
      public void setPrefix(String prefix) {
        this.prefix = prefix;
      }

      /**
       * Get whether includes package path part.
       * <p>
       * Default is {@code true}.
       * </p>
       *
       * @return If includes package path, return {@code true}
       */
      public boolean isIncludesPackagePath() {
        return includesPackagePath;
      }

      /**
       * Set whether includes package path part.
       *
       * @param includesPackagePath
       *          If want to includes, set {@code true}
       */
      public void setIncludesPackagePath(boolean includesPackagePath) {
        this.includesPackagePath = includesPackagePath;
      }

      /**
       * Get whether separate directory per mapper.
       *
       * @return If separate directory per mapper, return {@code true}
       */
      public boolean isSeparateDirectoryPerMapper() {
        return separateDirectoryPerMapper;
      }

      /**
       * Set whether separate directory per mapper.
       * <p>
       * Default is {@code true}.
       * </p>
       *
       * @param separateDirectoryPerMapper
       *          If want to separate directory, set {@code true}
       */
      public void setSeparateDirectoryPerMapper(boolean separateDirectoryPerMapper) {
        this.separateDirectoryPerMapper = separateDirectoryPerMapper;
      }

      /**
       * Get whether includes mapper name into file name when separate directory per mapper.
       * <p>
       * Default is {@code true}.
       * </p>
       *
       * @return If includes mapper name, return {@code true}
       */
      public boolean isIncludesMapperNameWhenSeparateDirectory() {
        return includesMapperNameWhenSeparateDirectory;
      }

      /**
       * Set whether includes mapper name into file name when separate directory per mapper.
       * <p>
       * Default is {@code true}.
       * </p>
       *
       * @param includesMapperNameWhenSeparateDirectory
       *          If want to includes, set {@code true}
       */
      public void setIncludesMapperNameWhenSeparateDirectory(boolean includesMapperNameWhenSeparateDirectory) {
        this.includesMapperNameWhenSeparateDirectory = includesMapperNameWhenSeparateDirectory;
      }

      /**
       * Get whether cache a resolved template file path.
       * <p>
       * Default is {@code true}.
       * </p>
       *
       * @return If cache a resolved template file path, return {@code true}
       */
      public boolean isCacheEnabled() {
        return cacheEnabled;
      }

      /**
       * Set whether cache a resolved template file path.
       *
       * @param cacheEnabled
       *          If want to cache, set {@code true}
       */
      public void setCacheEnabled(boolean cacheEnabled) {
        this.cacheEnabled = cacheEnabled;
      }

    }

  }

  /**
   * Create an instance from default properties file. <br>
   * If you want to customize a default {@code TemplateEngine}, you can configure some property using
   * mybatis-freemarker.properties that encoded by UTF-8. Also, you can change the properties file that will read using
   * system property (-Dmybatis-freemarker.config.file=... -Dmybatis-freemarker.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">General configuration</th>
   * </tr>
   * <tr>
   * <td>base-package</td>
   * <td>The base directory for reading template resources</td>
   * <td>None(just under classpath)</td>
   * </tr>
   * <tr>
   * <td>freemarker-settings.*</td>
   * <td>The settings of freemarker {@link freemarker.core.Configurable#setSetting(String, String)}).</td>
   * <td>-</td>
   * </tr>
   * </table>
   *
   * @return a configuration instance
   */
  public static FreeMarkerLanguageDriverConfig newInstance() {
    return newInstance(loadDefaultProperties());
  }

  /**
   * Create an instance from specified properties.
   *
   * @param customProperties
   *          custom configuration properties
   *
   * @return a configuration instance
   *
   * @see #newInstance()
   */
  public static FreeMarkerLanguageDriverConfig newInstance(Properties customProperties) {
    FreeMarkerLanguageDriverConfig config = new FreeMarkerLanguageDriverConfig();
    Properties properties = loadDefaultProperties();
    Optional.ofNullable(customProperties).ifPresent(properties::putAll);
    override(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 FreeMarkerLanguageDriverConfig newInstance(Consumer<FreeMarkerLanguageDriverConfig> customizer) {
    FreeMarkerLanguageDriverConfig config = new FreeMarkerLanguageDriverConfig();
    customizer.accept(config);
    override(config, loadDefaultProperties());
    return config;
  }

  private static void override(FreeMarkerLanguageDriverConfig config, Properties properties) {
    MetaObject metaObject = MetaObject.forObject(config, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(),
        new DefaultReflectorFactory());
    properties.forEach((key, value) -> {
      String propertyPath = WordUtils
          .uncapitalize(WordUtils.capitalize(Objects.toString(key), '-').replaceAll("-", ""));
      Optional.ofNullable(value).ifPresent(v -> {
        Object convertedValue = TYPE_CONVERTERS.get(metaObject.getSetterType(propertyPath)).apply(value.toString());
        metaObject.setValue(propertyPath, convertedValue);
      });
    });
  }

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

}