View Javadoc
1   /*
2    *    Copyright 2015-2025 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.CharArrayWriter;
19  import java.io.IOException;
20  import java.util.ArrayList;
21  import java.util.HashMap;
22  import java.util.List;
23  import java.util.Map;
24  
25  import org.apache.ibatis.builder.SqlSourceBuilder;
26  import org.apache.ibatis.mapping.BoundSql;
27  import org.apache.ibatis.mapping.SqlSource;
28  import org.apache.ibatis.session.Configuration;
29  
30  import freemarker.template.SimpleScalar;
31  import freemarker.template.Template;
32  import freemarker.template.TemplateException;
33  import freemarker.template.Version;
34  
35  /**
36   * Applies provided parameter(s) to FreeMarker template. Then passes the result into default MyBatis engine (and it
37   * finally replaces #{}-params to '?'-params). So, FreeMarker is used as preprocessor for MyBatis engine.
38   *
39   * @author elwood
40   */
41  public class FreeMarkerSqlSource implements SqlSource {
42    private final Template template;
43    private final Configuration configuration;
44    private final Version incompatibleImprovementsVersion;
45    private final String databaseId;
46  
47    public static final String GENERATED_PARAMS_KEY = "__GENERATED__";
48  
49    public FreeMarkerSqlSource(Template template, Configuration configuration, Version incompatibleImprovementsVersion) {
50      this.template = template;
51      this.configuration = configuration;
52      this.incompatibleImprovementsVersion = incompatibleImprovementsVersion;
53      this.databaseId = configuration.getDatabaseId();
54    }
55  
56    /**
57     * Populates additional parameters to data context. Data context can be {@link java.util.Map} or
58     * {@link org.mybatis.scripting.freemarker.ParamObjectAdapter} instance.
59     */
60    protected Object preProcessDataContext(Object dataContext, boolean isMap) {
61      if (isMap) {
62        ((Map<String, Object>) dataContext).put(MyBatisParamDirective.DEFAULT_KEY, new MyBatisParamDirective());
63        ((Map<String, Object>) dataContext).put(MyBatisParamDirective.DATABASE_ID_KEY, new SimpleScalar(this.databaseId));
64      } else {
65        ((ParamObjectAdapter) dataContext).putAdditionalParam(MyBatisParamDirective.DEFAULT_KEY,
66            new MyBatisParamDirective());
67        ((ParamObjectAdapter) dataContext).putAdditionalParam(MyBatisParamDirective.DATABASE_ID_KEY,
68            new SimpleScalar(this.databaseId));
69      }
70      return dataContext;
71    }
72  
73    @Override
74    public BoundSql getBoundSql(Object parameterObject) {
75      // Add to passed parameterObject our predefined directive - MyBatisParamDirective
76      // It will be available as "p" inside templates
77      Object dataContext;
78      List generatedParams = new ArrayList<>();
79      if (parameterObject != null) {
80        if (parameterObject instanceof Map) {
81          HashMap<String, Object> map = new HashMap<>((Map<String, Object>) parameterObject);
82          map.put(GENERATED_PARAMS_KEY, generatedParams);
83          dataContext = preProcessDataContext(map, true);
84        } else {
85          ParamObjectAdapter adapter = new ParamObjectAdapter(parameterObject, generatedParams,
86              incompatibleImprovementsVersion);
87          dataContext = preProcessDataContext(adapter, false);
88        }
89      } else {
90        HashMap<Object, Object> map = new HashMap<>();
91        map.put(GENERATED_PARAMS_KEY, generatedParams);
92        dataContext = preProcessDataContext(map, true);
93      }
94  
95      CharArrayWriter writer = new CharArrayWriter();
96      try {
97        template.process(dataContext, writer);
98      } catch (TemplateException | IOException e) {
99        throw new RuntimeException(e);
100     }
101 
102     // We got SQL ready for MyBatis here. This SQL contains
103     // params declarations like "#{param}",
104     // they will be replaced to '?' by MyBatis engine further
105     String sql = writer.toString();
106 
107     if (!generatedParams.isEmpty()) {
108       if (!(parameterObject instanceof Map)) {
109         throw new UnsupportedOperationException("Auto-generated prepared statements parameters"
110             + " are not available if using parameters object. Use @Param-annotated parameters" + " instead.");
111       }
112 
113       Map<String, Object> parametersMap = (Map<String, Object>) parameterObject;
114       for (int i = 0; i < generatedParams.size(); i++) {
115         parametersMap.put("_p" + i, generatedParams.get(i));
116       }
117     }
118 
119     // Pass retrieved SQL into MyBatis engine, it will substitute prepared-statements parameters
120     SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
121     Class<?> parameterType1 = parameterObject == null ? Object.class : parameterObject.getClass();
122     SqlSource sqlSource = sqlSourceParser.parse(sql, parameterType1, new HashMap<>());
123     return sqlSource.getBoundSql(parameterObject);
124   }
125 }