• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mybatis / thymeleaf-scripting / 915

15 Mar 2026 05:24PM UTC coverage: 96.169% (-1.1%) from 97.255%
915

Pull #302

github

web-flow
Merge 1f2c18208 into c20b3f258
Pull Request #302: Compatibility with core 3.6.0

135 of 146 branches covered (92.47%)

Branch coverage included in aggregate %.

30 of 33 new or added lines in 2 files covered. (90.91%)

6 existing lines in 1 file now uncovered.

618 of 637 relevant lines covered (97.02%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.0
/src/main/java/org/mybatis/scripting/thymeleaf/ThymeleafSqlSource.java
1
/*
2
 *    Copyright 2018-2026 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.thymeleaf;
17

18
import java.lang.reflect.InvocationTargetException;
19
import java.util.ArrayList;
20
import java.util.Arrays;
21
import java.util.Collection;
22
import java.util.HashMap;
23
import java.util.HashSet;
24
import java.util.List;
25
import java.util.Locale;
26
import java.util.Map;
27
import java.util.Optional;
28
import java.util.Properties;
29
import java.util.Set;
30
import java.util.function.BiFunction;
31

32
import org.apache.ibatis.builder.ParameterMappingTokenHandler;
33
import org.apache.ibatis.builder.SqlSourceBuilder;
34
import org.apache.ibatis.mapping.BoundSql;
35
import org.apache.ibatis.mapping.ParameterMapping;
36
import org.apache.ibatis.mapping.SqlSource;
37
import org.apache.ibatis.parsing.GenericTokenParser;
38
import org.apache.ibatis.reflection.MetaClass;
39
import org.apache.ibatis.reflection.ParamNameResolver;
40
import org.apache.ibatis.scripting.xmltags.DynamicContext;
41
import org.apache.ibatis.session.Configuration;
42
import org.thymeleaf.context.IContext;
43

44
/**
45
 * The {@code SqlSource} for integrating with Thymeleaf.
46
 *
47
 * @author Kazuki Shimizu
48
 *
49
 * @version 1.0.0
50
 *
51
 * @see ThymeleafLanguageDriver
52
 */
53
class ThymeleafSqlSource implements SqlSource {
54

55
  private static class TemporaryTakeoverKeys {
56
    private static final String CONFIGURATION = "__configuration__";
57
    private static final String DYNAMIC_CONTEXT = "__dynamicContext__";
58
    private static final String PROCESSING_PARAMETER_TYPE = "__processingParameterType__";
59
  }
60

61
  private final Configuration configuration;
62
  private final SqlGenerator sqlGenerator;
63
  private final String sqlTemplate;
64
  private final Class<?> parameterType;
65
  private final ParamNameResolver paramNameResolver;
66

67
  /**
68
   * Constructor for for integrating with template engine provide by Thymeleaf.
69
   *
70
   * @param configuration
71
   *          A configuration instance of MyBatis
72
   * @param sqlGenerator
73
   *          A sql generator using the Thymeleaf feature
74
   * @param sqlTemplate
75
   *          A template string of SQL (inline SQL or template file path)
76
   * @param parameterType
77
   *          A parameter type that specified at mapper method argument or xml element
78
   */
79
  ThymeleafSqlSource(Configuration configuration, SqlGenerator sqlGenerator, String sqlTemplate, Class<?> parameterType,
80
      ParamNameResolver paramNameResolver) {
1✔
81
    this.configuration = configuration;
1✔
82
    this.sqlGenerator = sqlGenerator;
1✔
83
    this.sqlTemplate = sqlTemplate;
1✔
84
    this.parameterType = parameterType;
1✔
85
    this.paramNameResolver = paramNameResolver;
1✔
86
  }
1✔
87

88
  @Deprecated
89
  ThymeleafSqlSource(Configuration configuration, SqlGenerator sqlGenerator, String sqlTemplate,
UNCOV
90
      Class<?> parameterType) {
×
UNCOV
91
    this.configuration = configuration;
×
UNCOV
92
    this.sqlGenerator = sqlGenerator;
×
UNCOV
93
    this.sqlTemplate = sqlTemplate;
×
UNCOV
94
    this.parameterType = parameterType;
×
NEW
95
    this.paramNameResolver = null;
×
UNCOV
96
  }
×
97

98
  /**
99
   * {@inheritDoc}
100
   */
101
  @Override
102
  public BoundSql getBoundSql(Object parameterObject) {
103
    Class<?> processingParameterType;
104
    if (parameterType == null) {
1✔
105
      processingParameterType = parameterObject == null ? Object.class : parameterObject.getClass();
1✔
106
    } else {
107
      processingParameterType = parameterType;
1✔
108
    }
109

110
    Map<String, Object> bindings = new HashMap<>();
1✔
111
    bindings.put(DynamicContext.PARAMETER_OBJECT_KEY, parameterObject);
1✔
112
    bindings.put(DynamicContext.DATABASE_ID_KEY, configuration.getDatabaseId());
1✔
113

114
    Map<String, Object> customVariables = bindings;
1✔
115
    customVariables.put(TemporaryTakeoverKeys.CONFIGURATION, configuration);
1✔
116
    customVariables.put(TemporaryTakeoverKeys.DYNAMIC_CONTEXT, bindings);
1✔
117
    customVariables.put(TemporaryTakeoverKeys.PROCESSING_PARAMETER_TYPE, processingParameterType);
1✔
118
    String sql = sqlGenerator.generate(sqlTemplate, parameterObject, bindings::put, customVariables);
1✔
119

120
    SqlSource sqlSource = parse(configuration, sql, parameterObject, bindings, paramNameResolver);
1✔
121
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
1✔
122
    bindings.forEach(boundSql::setAdditionalParameter);
1✔
123

124
    return boundSql;
1✔
125
  }
126

127
  private static SqlSource parse(Configuration configuration, String originalSql, Object parameterObject,
128
      Map<String, Object> additionalParameters, ParamNameResolver pnResolver) {
129
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
1✔
130
    List<ParameterMapping> parameterMappings = new ArrayList<>();
1✔
131
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(parameterMappings, configuration,
1✔
132
        parameterObject, parameterType, additionalParameters, pnResolver, false);
133
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
1✔
134
    return SqlSourceBuilder.buildSqlSource(configuration, parser.parse(originalSql), parameterMappings);
1✔
135
  }
136

137
  /**
138
   * The factory class for Thymeleaf's context.
139
   *
140
   * @since 1.0.2
141
   */
142
  static class ContextFactory implements BiFunction<Object, Map<String, Object>, IContext> {
1✔
143
    /**
144
     * {@inheritDoc}
145
     */
146
    @Override
147
    public IContext apply(Object parameter, Map<String, Object> customVariable) {
148
      Configuration configuration = (Configuration) customVariable.remove(TemporaryTakeoverKeys.CONFIGURATION);
1✔
149
      Map<String, Object> bindings = (Map<String, Object>) customVariable.remove(TemporaryTakeoverKeys.DYNAMIC_CONTEXT);
1✔
150
      Class<?> processingParameterType = (Class<?>) customVariable
1✔
151
          .remove(TemporaryTakeoverKeys.PROCESSING_PARAMETER_TYPE);
1✔
152
      MyBatisBindingContext bindingContext = new MyBatisBindingContext(
1✔
153
          parameter != null && configuration.getTypeHandlerRegistry().hasTypeHandler(processingParameterType));
1✔
154
      bindings.put(MyBatisBindingContext.CONTEXT_VARIABLE_NAME, bindingContext);
1✔
155
      IContext context;
156
      if (parameter instanceof Map) {
1✔
157
        @SuppressWarnings(value = "unchecked")
158
        Map<String, Object> map = (Map<String, Object>) parameter;
1✔
159
        context = new MapBasedContext(map, bindings, configuration.getVariables());
1✔
160
      } else {
1✔
161
        MetaClass metaClass = MetaClass.forClass(processingParameterType, configuration.getReflectorFactory());
1✔
162
        context = new MetaClassBasedContext(parameter, metaClass, processingParameterType, bindings,
1✔
163
            configuration.getVariables());
1✔
164
      }
165
      return context;
1✔
166
    }
167
  }
168

169
  private abstract static class AbstractContext implements IContext {
170

171
    private final Map<String, Object> dynamicContext;
172
    private final Properties configurationProperties;
173
    private final Set<String> variableNames;
174

175
    private AbstractContext(Map<String, Object> dynamicContext, Properties configurationProperties) {
1✔
176
      this.dynamicContext = dynamicContext;
1✔
177
      this.configurationProperties = configurationProperties;
1✔
178
      this.variableNames = new HashSet<>();
1✔
179
      addVariableNames(dynamicContext.keySet());
1✔
180
      Optional.ofNullable(configurationProperties).ifPresent(v -> addVariableNames(v.stringPropertyNames()));
1✔
181
    }
1✔
182

183
    void addVariableNames(Collection<String> names) {
184
      variableNames.addAll(names);
1✔
185
    }
1✔
186

187
    /**
188
     * {@inheritDoc}
189
     */
190
    @Override
191
    public Locale getLocale() {
192
      return Locale.getDefault();
1✔
193
    }
194

195
    /**
196
     * {@inheritDoc}
197
     */
198
    @Override
199
    public boolean containsVariable(String name) {
200
      return variableNames.contains(name);
×
201
    }
202

203
    /**
204
     * {@inheritDoc}
205
     */
206
    @Override
207
    public Set<String> getVariableNames() {
208
      return variableNames;
1✔
209
    }
210

211
    /**
212
     * {@inheritDoc}
213
     */
214
    @Override
215
    public Object getVariable(String name) {
216
      if (dynamicContext.containsKey(name)) {
1✔
217
        return dynamicContext.get(name);
1✔
218
      }
219
      if (configurationProperties != null && configurationProperties.containsKey(name)) {
1✔
220
        return configurationProperties.getProperty(name);
1✔
221
      }
222
      return getParameterValue(name);
1✔
223
    }
224

225
    abstract Object getParameterValue(String name);
226

227
  }
228

229
  private static class MapBasedContext extends AbstractContext {
230

231
    private final Map<String, Object> variables;
232

233
    private MapBasedContext(Map<String, Object> parameterMap, Map<String, Object> dynamicContext,
234
        Properties configurationProperties) {
235
      super(dynamicContext, configurationProperties);
1✔
236
      this.variables = parameterMap;
1✔
237
      addVariableNames(parameterMap.keySet());
1✔
238
    }
1✔
239

240
    /**
241
     * {@inheritDoc}
242
     */
243
    @Override
244
    public Object getParameterValue(String name) {
245
      return variables.get(name);
1✔
246
    }
247

248
  }
249

250
  private static class MetaClassBasedContext extends AbstractContext {
251

252
    private final Object parameterObject;
253
    private final MetaClass parameterMetaClass;
254
    private final Class<?> parameterType;
255

256
    private MetaClassBasedContext(Object parameterObject, MetaClass parameterMetaClass, Class<?> parameterType,
257
        Map<String, Object> dynamicContext, Properties configurationProperties) {
258
      super(dynamicContext, configurationProperties);
1✔
259
      this.parameterObject = parameterObject;
1✔
260
      this.parameterMetaClass = parameterMetaClass;
1✔
261
      this.parameterType = parameterType;
1✔
262
      addVariableNames(Arrays.asList(parameterMetaClass.getGetterNames()));
1✔
263
    }
1✔
264

265
    /**
266
     * {@inheritDoc}
267
     */
268
    @Override
269
    public Object getParameterValue(String name) {
270
      try {
271
        return parameterMetaClass.getGetInvoker(name).invoke(parameterObject, null);
1✔
272
      } catch (IllegalAccessException | InvocationTargetException e) {
1✔
273
        throw new IllegalStateException(
1✔
274
            String.format("Cannot get a value for property named '%s' in '%s'", name, parameterType), e);
1✔
275
      }
276
    }
277

278
  }
279

280
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc