View Javadoc
1   /*
2    *    Copyright 2009-2024 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.apache.ibatis.builder;
17  
18  import java.sql.ResultSet;
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Properties;
26  import java.util.Set;
27  import java.util.StringTokenizer;
28  
29  import org.apache.ibatis.cache.Cache;
30  import org.apache.ibatis.cache.decorators.LruCache;
31  import org.apache.ibatis.cache.impl.PerpetualCache;
32  import org.apache.ibatis.executor.ErrorContext;
33  import org.apache.ibatis.executor.keygen.KeyGenerator;
34  import org.apache.ibatis.mapping.CacheBuilder;
35  import org.apache.ibatis.mapping.Discriminator;
36  import org.apache.ibatis.mapping.MappedStatement;
37  import org.apache.ibatis.mapping.ParameterMap;
38  import org.apache.ibatis.mapping.ParameterMapping;
39  import org.apache.ibatis.mapping.ParameterMode;
40  import org.apache.ibatis.mapping.ResultFlag;
41  import org.apache.ibatis.mapping.ResultMap;
42  import org.apache.ibatis.mapping.ResultMapping;
43  import org.apache.ibatis.mapping.ResultSetType;
44  import org.apache.ibatis.mapping.SqlCommandType;
45  import org.apache.ibatis.mapping.SqlSource;
46  import org.apache.ibatis.mapping.StatementType;
47  import org.apache.ibatis.reflection.MetaClass;
48  import org.apache.ibatis.scripting.LanguageDriver;
49  import org.apache.ibatis.session.Configuration;
50  import org.apache.ibatis.type.JdbcType;
51  import org.apache.ibatis.type.TypeHandler;
52  
53  /**
54   * @author Clinton Begin
55   */
56  public class MapperBuilderAssistant extends BaseBuilder {
57  
58    private String currentNamespace;
59    private final String resource;
60    private Cache currentCache;
61    private boolean unresolvedCacheRef; // issue #676
62  
63    public MapperBuilderAssistant(Configuration configuration, String resource) {
64      super(configuration);
65      ErrorContext.instance().resource(resource);
66      this.resource = resource;
67    }
68  
69    public String getCurrentNamespace() {
70      return currentNamespace;
71    }
72  
73    public void setCurrentNamespace(String currentNamespace) {
74      if (currentNamespace == null) {
75        throw new BuilderException("The mapper element requires a namespace attribute to be specified.");
76      }
77  
78      if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) {
79        throw new BuilderException(
80            "Wrong namespace. Expected '" + this.currentNamespace + "' but found '" + currentNamespace + "'.");
81      }
82  
83      this.currentNamespace = currentNamespace;
84    }
85  
86    public String applyCurrentNamespace(String base, boolean isReference) {
87      if (base == null) {
88        return null;
89      }
90      if (isReference) {
91        // is it qualified with any namespace yet?
92        if (base.contains(".")) {
93          return base;
94        }
95      } else {
96        // is it qualified with this namespace yet?
97        if (base.startsWith(currentNamespace + ".")) {
98          return base;
99        }
100       if (base.contains(".")) {
101         throw new BuilderException("Dots are not allowed in element names, please remove it from " + base);
102       }
103     }
104     return currentNamespace + "." + base;
105   }
106 
107   public Cache useCacheRef(String namespace) {
108     if (namespace == null) {
109       throw new BuilderException("cache-ref element requires a namespace attribute.");
110     }
111     try {
112       unresolvedCacheRef = true;
113       Cache cache = configuration.getCache(namespace);
114       if (cache == null) {
115         throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
116       }
117       currentCache = cache;
118       unresolvedCacheRef = false;
119       return cache;
120     } catch (IllegalArgumentException e) {
121       throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
122     }
123   }
124 
125   public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval,
126       Integer size, boolean readWrite, boolean blocking, Properties props) {
127     Cache cache = new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class))
128         .addDecorator(valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size)
129         .readWrite(readWrite).blocking(blocking).properties(props).build();
130     configuration.addCache(cache);
131     currentCache = cache;
132     return cache;
133   }
134 
135   public ParameterMap addParameterMap(String id, Class<?> parameterClass, List<ParameterMapping> parameterMappings) {
136     id = applyCurrentNamespace(id, false);
137     ParameterMap parameterMap = new ParameterMap.Builder(configuration, id, parameterClass, parameterMappings).build();
138     configuration.addParameterMap(parameterMap);
139     return parameterMap;
140   }
141 
142   public ParameterMapping buildParameterMapping(Class<?> parameterType, String property, Class<?> javaType,
143       JdbcType jdbcType, String resultMap, ParameterMode parameterMode, Class<? extends TypeHandler<?>> typeHandler,
144       Integer numericScale) {
145     resultMap = applyCurrentNamespace(resultMap, true);
146 
147     // Class parameterType = parameterMapBuilder.type();
148     Class<?> javaTypeClass = resolveParameterJavaType(parameterType, property, javaType, jdbcType);
149     TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
150 
151     return new ParameterMapping.Builder(configuration, property, javaTypeClass).jdbcType(jdbcType)
152         .resultMapId(resultMap).mode(parameterMode).numericScale(numericScale).typeHandler(typeHandlerInstance).build();
153   }
154 
155   public ResultMap addResultMap(String id, Class<?> type, String extend, Discriminator discriminator,
156       List<ResultMapping> resultMappings, Boolean autoMapping) {
157     id = applyCurrentNamespace(id, false);
158     extend = applyCurrentNamespace(extend, true);
159 
160     if (extend != null) {
161       if (!configuration.hasResultMap(extend)) {
162         throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
163       }
164       ResultMap resultMap = configuration.getResultMap(extend);
165       List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
166       extendedResultMappings.removeAll(resultMappings);
167       // Remove parent constructor if this resultMap declares a constructor.
168       boolean declaresConstructor = false;
169       for (ResultMapping resultMapping : resultMappings) {
170         if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
171           declaresConstructor = true;
172           break;
173         }
174       }
175       if (declaresConstructor) {
176         extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
177       }
178       resultMappings.addAll(extendedResultMappings);
179     }
180     ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
181         .discriminator(discriminator).build();
182     configuration.addResultMap(resultMap);
183     return resultMap;
184   }
185 
186   public Discriminator buildDiscriminator(Class<?> resultType, String column, Class<?> javaType, JdbcType jdbcType,
187       Class<? extends TypeHandler<?>> typeHandler, Map<String, String> discriminatorMap) {
188     ResultMapping resultMapping = buildResultMapping(resultType, null, column, javaType, jdbcType, null, null, null,
189         null, typeHandler, new ArrayList<>(), null, null, false);
190     Map<String, String> namespaceDiscriminatorMap = new HashMap<>();
191     for (Map.Entry<String, String> e : discriminatorMap.entrySet()) {
192       String resultMap = e.getValue();
193       resultMap = applyCurrentNamespace(resultMap, true);
194       namespaceDiscriminatorMap.put(e.getKey(), resultMap);
195     }
196     return new Discriminator.Builder(configuration, resultMapping, namespaceDiscriminatorMap).build();
197   }
198 
199   public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType,
200       SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType,
201       String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache,
202       boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId,
203       LanguageDriver lang, String resultSets, boolean dirtySelect) {
204 
205     if (unresolvedCacheRef) {
206       throw new IncompleteElementException("Cache-ref not yet resolved");
207     }
208 
209     id = applyCurrentNamespace(id, false);
210 
211     MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
212         .resource(resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType)
213         .keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId).lang(lang)
214         .resultOrdered(resultOrdered).resultSets(resultSets)
215         .resultMaps(getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType)
216         .flushCacheRequired(flushCache).useCache(useCache).cache(currentCache).dirtySelect(dirtySelect);
217 
218     ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
219     if (statementParameterMap != null) {
220       statementBuilder.parameterMap(statementParameterMap);
221     }
222 
223     MappedStatement statement = statementBuilder.build();
224     configuration.addMappedStatement(statement);
225     return statement;
226   }
227 
228   /**
229    * Backward compatibility signature 'addMappedStatement'.
230    *
231    * @param id
232    *          the id
233    * @param sqlSource
234    *          the sql source
235    * @param statementType
236    *          the statement type
237    * @param sqlCommandType
238    *          the sql command type
239    * @param fetchSize
240    *          the fetch size
241    * @param timeout
242    *          the timeout
243    * @param parameterMap
244    *          the parameter map
245    * @param parameterType
246    *          the parameter type
247    * @param resultMap
248    *          the result map
249    * @param resultType
250    *          the result type
251    * @param resultSetType
252    *          the result set type
253    * @param flushCache
254    *          the flush cache
255    * @param useCache
256    *          the use cache
257    * @param resultOrdered
258    *          the result ordered
259    * @param keyGenerator
260    *          the key generator
261    * @param keyProperty
262    *          the key property
263    * @param keyColumn
264    *          the key column
265    * @param databaseId
266    *          the database id
267    * @param lang
268    *          the lang
269    *
270    * @return the mapped statement
271    */
272   public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType,
273       SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType,
274       String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache,
275       boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId,
276       LanguageDriver lang, String resultSets) {
277     return addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
278         parameterType, resultMap, resultType, resultSetType, flushCache, useCache, resultOrdered, keyGenerator,
279         keyProperty, keyColumn, databaseId, lang, null, false);
280   }
281 
282   public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType,
283       SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType,
284       String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache,
285       boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId,
286       LanguageDriver lang) {
287     return addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
288         parameterType, resultMap, resultType, resultSetType, flushCache, useCache, resultOrdered, keyGenerator,
289         keyProperty, keyColumn, databaseId, lang, null);
290   }
291 
292   private <T> T valueOrDefault(T value, T defaultValue) {
293     return value == null ? defaultValue : value;
294   }
295 
296   private ParameterMap getStatementParameterMap(String parameterMapName, Class<?> parameterTypeClass,
297       String statementId) {
298     parameterMapName = applyCurrentNamespace(parameterMapName, true);
299     ParameterMap parameterMap = null;
300     if (parameterMapName != null) {
301       try {
302         parameterMap = configuration.getParameterMap(parameterMapName);
303       } catch (IllegalArgumentException e) {
304         throw new IncompleteElementException("Could not find parameter map " + parameterMapName, e);
305       }
306     } else if (parameterTypeClass != null) {
307       List<ParameterMapping> parameterMappings = new ArrayList<>();
308       parameterMap = new ParameterMap.Builder(configuration, statementId + "-Inline", parameterTypeClass,
309           parameterMappings).build();
310     }
311     return parameterMap;
312   }
313 
314   private List<ResultMap> getStatementResultMaps(String resultMap, Class<?> resultType, String statementId) {
315     resultMap = applyCurrentNamespace(resultMap, true);
316 
317     List<ResultMap> resultMaps = new ArrayList<>();
318     if (resultMap != null) {
319       String[] resultMapNames = resultMap.split(",");
320       for (String resultMapName : resultMapNames) {
321         try {
322           resultMaps.add(configuration.getResultMap(resultMapName.trim()));
323         } catch (IllegalArgumentException e) {
324           throw new IncompleteElementException(
325               "Could not find result map '" + resultMapName + "' referenced from '" + statementId + "'", e);
326         }
327       }
328     } else if (resultType != null) {
329       ResultMap inlineResultMap = new ResultMap.Builder(configuration, statementId + "-Inline", resultType,
330           new ArrayList<>(), null).build();
331       resultMaps.add(inlineResultMap);
332     }
333     return resultMaps;
334   }
335 
336   public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, Class<?> javaType,
337       JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,
338       Class<? extends TypeHandler<?>> typeHandler, List<ResultFlag> flags, String resultSet, String foreignColumn,
339       boolean lazy) {
340     Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
341     TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
342     List<ResultMapping> composites;
343     if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
344       composites = Collections.emptyList();
345     } else {
346       composites = parseCompositeColumnName(column);
347     }
348     return new ResultMapping.Builder(configuration, property, column, javaTypeClass).jdbcType(jdbcType)
349         .nestedQueryId(applyCurrentNamespace(nestedSelect, true))
350         .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true)).resultSet(resultSet)
351         .typeHandler(typeHandlerInstance).flags(flags == null ? new ArrayList<>() : flags).composites(composites)
352         .notNullColumns(parseMultipleColumnNames(notNullColumn)).columnPrefix(columnPrefix).foreignColumn(foreignColumn)
353         .lazy(lazy).build();
354   }
355 
356   /**
357    * Backward compatibility signature 'buildResultMapping'.
358    *
359    * @param resultType
360    *          the result type
361    * @param property
362    *          the property
363    * @param column
364    *          the column
365    * @param javaType
366    *          the java type
367    * @param jdbcType
368    *          the jdbc type
369    * @param nestedSelect
370    *          the nested select
371    * @param nestedResultMap
372    *          the nested result map
373    * @param notNullColumn
374    *          the not null column
375    * @param columnPrefix
376    *          the column prefix
377    * @param typeHandler
378    *          the type handler
379    * @param flags
380    *          the flags
381    *
382    * @return the result mapping
383    */
384   public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, Class<?> javaType,
385       JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,
386       Class<? extends TypeHandler<?>> typeHandler, List<ResultFlag> flags) {
387     return buildResultMapping(resultType, property, column, javaType, jdbcType, nestedSelect, nestedResultMap,
388         notNullColumn, columnPrefix, typeHandler, flags, null, null, configuration.isLazyLoadingEnabled());
389   }
390 
391   /**
392    * Gets the language driver.
393    *
394    * @param langClass
395    *          the lang class
396    *
397    * @return the language driver
398    *
399    * @deprecated Use {@link Configuration#getLanguageDriver(Class)}
400    */
401   @Deprecated
402   public LanguageDriver getLanguageDriver(Class<? extends LanguageDriver> langClass) {
403     return configuration.getLanguageDriver(langClass);
404   }
405 
406   private Set<String> parseMultipleColumnNames(String columnName) {
407     Set<String> columns = new HashSet<>();
408     if (columnName != null) {
409       if (columnName.indexOf(',') > -1) {
410         StringTokenizer parser = new StringTokenizer(columnName, "{}, ", false);
411         while (parser.hasMoreTokens()) {
412           String column = parser.nextToken();
413           columns.add(column);
414         }
415       } else {
416         columns.add(columnName);
417       }
418     }
419     return columns;
420   }
421 
422   private List<ResultMapping> parseCompositeColumnName(String columnName) {
423     List<ResultMapping> composites = new ArrayList<>();
424     if (columnName != null && (columnName.indexOf('=') > -1 || columnName.indexOf(',') > -1)) {
425       StringTokenizer parser = new StringTokenizer(columnName, "{}=, ", false);
426       while (parser.hasMoreTokens()) {
427         String property = parser.nextToken();
428         String column = parser.nextToken();
429         ResultMapping complexResultMapping = new ResultMapping.Builder(configuration, property, column,
430             configuration.getTypeHandlerRegistry().getUnknownTypeHandler()).build();
431         composites.add(complexResultMapping);
432       }
433     }
434     return composites;
435   }
436 
437   private Class<?> resolveResultJavaType(Class<?> resultType, String property, Class<?> javaType) {
438     if (javaType == null && property != null) {
439       try {
440         MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
441         javaType = metaResultType.getSetterType(property);
442       } catch (Exception e) {
443         // ignore, following null check statement will deal with the situation
444       }
445     }
446     if (javaType == null) {
447       javaType = Object.class;
448     }
449     return javaType;
450   }
451 
452   private Class<?> resolveParameterJavaType(Class<?> resultType, String property, Class<?> javaType,
453       JdbcType jdbcType) {
454     if (javaType == null) {
455       if (JdbcType.CURSOR.equals(jdbcType)) {
456         javaType = ResultSet.class;
457       } else if (Map.class.isAssignableFrom(resultType)) {
458         javaType = Object.class;
459       } else {
460         MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
461         javaType = metaResultType.getGetterType(property);
462       }
463     }
464     if (javaType == null) {
465       javaType = Object.class;
466     }
467     return javaType;
468   }
469 
470 }