View Javadoc
1   /*
2    *    Copyright 2015-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.freemarker;
17  
18  import java.io.IOException;
19  import java.io.StringReader;
20  import java.nio.charset.StandardCharsets;
21  
22  import org.apache.ibatis.executor.parameter.ParameterHandler;
23  import org.apache.ibatis.mapping.BoundSql;
24  import org.apache.ibatis.mapping.MappedStatement;
25  import org.apache.ibatis.mapping.SqlSource;
26  import org.apache.ibatis.parsing.XNode;
27  import org.apache.ibatis.scripting.LanguageDriver;
28  import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
29  import org.apache.ibatis.session.Configuration;
30  import org.mybatis.scripting.freemarker.support.TemplateFilePathProvider;
31  
32  import freemarker.cache.ClassTemplateLoader;
33  import freemarker.cache.TemplateLoader;
34  import freemarker.template.Template;
35  import freemarker.template.TemplateException;
36  
37  /**
38   * Adds FreeMarker templates support to scripting in MyBatis. If you want to change or extend template loader
39   * configuration, use can inherit from this class and override {@link #createFreeMarkerConfiguration()} method.
40   *
41   * @author elwood
42   * @author Kazuki Shimizu
43   */
44  public class FreeMarkerLanguageDriver implements LanguageDriver {
45  
46    protected final FreeMarkerLanguageDriverConfig driverConfig;
47    protected final freemarker.template.Configuration freemarkerCfg;
48  
49    /**
50     * Constructor.
51     *
52     * @see FreeMarkerLanguageDriverConfig#newInstance()
53     */
54    public FreeMarkerLanguageDriver() {
55      this(FreeMarkerLanguageDriverConfig.newInstance());
56    }
57  
58    /**
59     * Constructor.
60     *
61     * @param driverConfig
62     *          a language driver configuration
63     *
64     * @since 1.2.0
65     */
66    public FreeMarkerLanguageDriver(FreeMarkerLanguageDriverConfig driverConfig) {
67      this.driverConfig = driverConfig;
68      this.freemarkerCfg = createFreeMarkerConfiguration();
69      TemplateFilePathProvider.setLanguageDriverConfig(driverConfig);
70    }
71  
72    /**
73     * Creates the {@link freemarker.template.Configuration} instance and sets it up. If you want to change it (set
74     * another props, for example), you can override it in inherited class and use your own class in @Lang directive.
75     */
76    protected freemarker.template.Configuration createFreeMarkerConfiguration() {
77      freemarker.template.Configuration cfg = new freemarker.template.Configuration(
78          freemarker.template.Configuration.VERSION_2_3_22);
79  
80      TemplateLoader templateLoader = new ClassTemplateLoader(this.getClass().getClassLoader(),
81          driverConfig.getTemplateFile().getBaseDir());
82      cfg.setTemplateLoader(templateLoader);
83  
84      // To avoid formatting numbers using spaces and commas in SQL
85      cfg.setNumberFormat("computer");
86  
87      // Because it defaults to default system encoding, we should set it always explicitly
88      cfg.setDefaultEncoding(StandardCharsets.UTF_8.name());
89  
90      driverConfig.getFreemarkerSettings().forEach((name, value) -> {
91        try {
92          cfg.setSetting(name, value);
93        } catch (TemplateException e) {
94          throw new IllegalStateException(
95              String.format("Fail to configure FreeMarker template setting. name[%s] value[%s]", name, value), e);
96        }
97      });
98  
99      return cfg;
100   }
101 
102   /**
103    * Creates a {@link ParameterHandler} that passes the actual parameters to the the JDBC statement.
104    *
105    * @see DefaultParameterHandler
106    *
107    * @param mappedStatement
108    *          The mapped statement that is being executed
109    * @param parameterObject
110    *          The input parameter object (can be null)
111    * @param boundSql
112    *          The resulting SQL once the dynamic language has been executed.
113    */
114   @Override
115   public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject,
116       BoundSql boundSql) {
117     // As default XMLLanguageDriver
118     return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
119   }
120 
121   /**
122    * Creates an {@link SqlSource} that will hold the statement read from a mapper xml file. It is called during startup,
123    * when the mapped statement is read from a class or an xml file.
124    *
125    * @param configuration
126    *          The MyBatis configuration
127    * @param script
128    *          XNode parsed from a XML file
129    * @param parameterType
130    *          input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be
131    *          null.
132    */
133   @Override
134   public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
135     return createSqlSource(configuration, script.getNode().getTextContent());
136   }
137 
138   /**
139    * Creates an {@link SqlSource} that will hold the statement read from an annotation. It is called during startup,
140    * when the mapped statement is read from a class or an xml file.
141    *
142    * @param configuration
143    *          The MyBatis configuration
144    * @param script
145    *          The content of the annotation
146    * @param parameterType
147    *          input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be
148    *          null.
149    */
150   @Override
151   public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
152     return createSqlSource(configuration, script);
153   }
154 
155   protected SqlSource createSqlSource(Template template, Configuration configuration) {
156     return new FreeMarkerSqlSource(template, configuration, freemarkerCfg.getIncompatibleImprovements());
157   }
158 
159   private SqlSource createSqlSource(Configuration configuration, String scriptText) {
160     Template template;
161     if (scriptText.trim().contains(" ")) {
162       // Consider that script is inline script
163       try {
164         template = new Template(null, new StringReader(scriptText), freemarkerCfg);
165       } catch (IOException e) {
166         throw new RuntimeException(e);
167       }
168     } else {
169       // Consider that script is template name, trying to find the template in classpath
170       try {
171         template = freemarkerCfg.getTemplate(scriptText.trim());
172       } catch (IOException e) {
173         throw new RuntimeException(e);
174       }
175     }
176 
177     return createSqlSource(template, configuration);
178   }
179 
180 }