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