View Javadoc
1   /*
2    *    Copyright 2009-2021 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    *       http://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("Wrong namespace. Expected '"
79            + 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,
125       Class<? extends Cache> evictionClass,
126       Long flushInterval,
127       Integer size,
128       boolean readWrite,
129       boolean blocking,
130       Properties props) {
131     Cache cache = new CacheBuilder(currentNamespace)
132         .implementation(valueOrDefault(typeClass, PerpetualCache.class))
133         .addDecorator(valueOrDefault(evictionClass, LruCache.class))
134         .clearInterval(flushInterval)
135         .size(size)
136         .readWrite(readWrite)
137         .blocking(blocking)
138         .properties(props)
139         .build();
140     configuration.addCache(cache);
141     currentCache = cache;
142     return cache;
143   }
144 
145   public ParameterMap addParameterMap(String id, Class<?> parameterClass, List<ParameterMapping> parameterMappings) {
146     id = applyCurrentNamespace(id, false);
147     ParameterMap parameterMap = new ParameterMap.Builder(configuration, id, parameterClass, parameterMappings).build();
148     configuration.addParameterMap(parameterMap);
149     return parameterMap;
150   }
151 
152   public ParameterMapping buildParameterMapping(
153       Class<?> parameterType,
154       String property,
155       Class<?> javaType,
156       JdbcType jdbcType,
157       String resultMap,
158       ParameterMode parameterMode,
159       Class<? extends TypeHandler<?>> typeHandler,
160       Integer numericScale) {
161     resultMap = applyCurrentNamespace(resultMap, true);
162 
163     // Class parameterType = parameterMapBuilder.type();
164     Class<?> javaTypeClass = resolveParameterJavaType(parameterType, property, javaType, jdbcType);
165     TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
166 
167     return new ParameterMapping.Builder(configuration, property, javaTypeClass)
168         .jdbcType(jdbcType)
169         .resultMapId(resultMap)
170         .mode(parameterMode)
171         .numericScale(numericScale)
172         .typeHandler(typeHandlerInstance)
173         .build();
174   }
175 
176   public ResultMap addResultMap(
177       String id,
178       Class<?> type,
179       String extend,
180       Discriminator discriminator,
181       List<ResultMapping> resultMappings,
182       Boolean autoMapping) {
183     id = applyCurrentNamespace(id, false);
184     extend = applyCurrentNamespace(extend, true);
185 
186     if (extend != null) {
187       if (!configuration.hasResultMap(extend)) {
188         throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
189       }
190       ResultMap resultMap = configuration.getResultMap(extend);
191       List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
192       extendedResultMappings.removeAll(resultMappings);
193       // Remove parent constructor if this resultMap declares a constructor.
194       boolean declaresConstructor = false;
195       for (ResultMapping resultMapping : resultMappings) {
196         if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
197           declaresConstructor = true;
198           break;
199         }
200       }
201       if (declaresConstructor) {
202         extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
203       }
204       resultMappings.addAll(extendedResultMappings);
205     }
206     ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
207         .discriminator(discriminator)
208         .build();
209     configuration.addResultMap(resultMap);
210     return resultMap;
211   }
212 
213   public Discriminator buildDiscriminator(
214       Class<?> resultType,
215       String column,
216       Class<?> javaType,
217       JdbcType jdbcType,
218       Class<? extends TypeHandler<?>> typeHandler,
219       Map<String, String> discriminatorMap) {
220     ResultMapping resultMapping = buildResultMapping(
221         resultType,
222         null,
223         column,
224         javaType,
225         jdbcType,
226         null,
227         null,
228         null,
229         null,
230         typeHandler,
231         new ArrayList<>(),
232         null,
233         null,
234         false);
235     Map<String, String> namespaceDiscriminatorMap = new HashMap<>();
236     for (Map.Entry<String, String> e : discriminatorMap.entrySet()) {
237       String resultMap = e.getValue();
238       resultMap = applyCurrentNamespace(resultMap, true);
239       namespaceDiscriminatorMap.put(e.getKey(), resultMap);
240     }
241     return new Discriminator.Builder(configuration, resultMapping, namespaceDiscriminatorMap).build();
242   }
243 
244   public MappedStatement addMappedStatement(
245       String id,
246       SqlSource sqlSource,
247       StatementType statementType,
248       SqlCommandType sqlCommandType,
249       Integer fetchSize,
250       Integer timeout,
251       String parameterMap,
252       Class<?> parameterType,
253       String resultMap,
254       Class<?> resultType,
255       ResultSetType resultSetType,
256       boolean flushCache,
257       boolean useCache,
258       boolean resultOrdered,
259       KeyGenerator keyGenerator,
260       String keyProperty,
261       String keyColumn,
262       String databaseId,
263       LanguageDriver lang,
264       String resultSets) {
265 
266     if (unresolvedCacheRef) {
267       throw new IncompleteElementException("Cache-ref not yet resolved");
268     }
269 
270     id = applyCurrentNamespace(id, false);
271     boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
272 
273     MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
274         .resource(resource)
275         .fetchSize(fetchSize)
276         .timeout(timeout)
277         .statementType(statementType)
278         .keyGenerator(keyGenerator)
279         .keyProperty(keyProperty)
280         .keyColumn(keyColumn)
281         .databaseId(databaseId)
282         .lang(lang)
283         .resultOrdered(resultOrdered)
284         .resultSets(resultSets)
285         .resultMaps(getStatementResultMaps(resultMap, resultType, id))
286         .resultSetType(resultSetType)
287         .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
288         .useCache(valueOrDefault(useCache, isSelect))
289         .cache(currentCache);
290 
291     ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
292     if (statementParameterMap != null) {
293       statementBuilder.parameterMap(statementParameterMap);
294     }
295 
296     MappedStatement statement = statementBuilder.build();
297     configuration.addMappedStatement(statement);
298     return statement;
299   }
300 
301   /**
302    * Backward compatibility signature 'addMappedStatement'.
303    *
304    * @param id
305    *          the id
306    * @param sqlSource
307    *          the sql source
308    * @param statementType
309    *          the statement type
310    * @param sqlCommandType
311    *          the sql command type
312    * @param fetchSize
313    *          the fetch size
314    * @param timeout
315    *          the timeout
316    * @param parameterMap
317    *          the parameter map
318    * @param parameterType
319    *          the parameter type
320    * @param resultMap
321    *          the result map
322    * @param resultType
323    *          the result type
324    * @param resultSetType
325    *          the result set type
326    * @param flushCache
327    *          the flush cache
328    * @param useCache
329    *          the use cache
330    * @param resultOrdered
331    *          the result ordered
332    * @param keyGenerator
333    *          the key generator
334    * @param keyProperty
335    *          the key property
336    * @param keyColumn
337    *          the key column
338    * @param databaseId
339    *          the database id
340    * @param lang
341    *          the lang
342    * @return the mapped statement
343    */
344   public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType,
345       SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType,
346       String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache,
347       boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId,
348       LanguageDriver lang) {
349     return addMappedStatement(
350       id, sqlSource, statementType, sqlCommandType, fetchSize, timeout,
351       parameterMap, parameterType, resultMap, resultType, resultSetType,
352       flushCache, useCache, resultOrdered, keyGenerator, keyProperty,
353       keyColumn, databaseId, lang, null);
354   }
355 
356   private <T> T valueOrDefault(T value, T defaultValue) {
357     return value == null ? defaultValue : value;
358   }
359 
360   private ParameterMap getStatementParameterMap(
361       String parameterMapName,
362       Class<?> parameterTypeClass,
363       String statementId) {
364     parameterMapName = applyCurrentNamespace(parameterMapName, true);
365     ParameterMap parameterMap = null;
366     if (parameterMapName != null) {
367       try {
368         parameterMap = configuration.getParameterMap(parameterMapName);
369       } catch (IllegalArgumentException e) {
370         throw new IncompleteElementException("Could not find parameter map " + parameterMapName, e);
371       }
372     } else if (parameterTypeClass != null) {
373       List<ParameterMapping> parameterMappings = new ArrayList<>();
374       parameterMap = new ParameterMap.Builder(
375           configuration,
376           statementId + "-Inline",
377           parameterTypeClass,
378           parameterMappings).build();
379     }
380     return parameterMap;
381   }
382 
383   private List<ResultMap> getStatementResultMaps(
384       String resultMap,
385       Class<?> resultType,
386       String statementId) {
387     resultMap = applyCurrentNamespace(resultMap, true);
388 
389     List<ResultMap> resultMaps = new ArrayList<>();
390     if (resultMap != null) {
391       String[] resultMapNames = resultMap.split(",");
392       for (String resultMapName : resultMapNames) {
393         try {
394           resultMaps.add(configuration.getResultMap(resultMapName.trim()));
395         } catch (IllegalArgumentException e) {
396           throw new IncompleteElementException("Could not find result map '" + resultMapName + "' referenced from '" + statementId + "'", e);
397         }
398       }
399     } else if (resultType != null) {
400       ResultMap inlineResultMap = new ResultMap.Builder(
401           configuration,
402           statementId + "-Inline",
403           resultType,
404           new ArrayList<>(),
405           null).build();
406       resultMaps.add(inlineResultMap);
407     }
408     return resultMaps;
409   }
410 
411   public ResultMapping buildResultMapping(
412       Class<?> resultType,
413       String property,
414       String column,
415       Class<?> javaType,
416       JdbcType jdbcType,
417       String nestedSelect,
418       String nestedResultMap,
419       String notNullColumn,
420       String columnPrefix,
421       Class<? extends TypeHandler<?>> typeHandler,
422       List<ResultFlag> flags,
423       String resultSet,
424       String foreignColumn,
425       boolean lazy) {
426     Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
427     TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
428     List<ResultMapping> composites;
429     if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
430       composites = Collections.emptyList();
431     } else {
432       composites = parseCompositeColumnName(column);
433     }
434     return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
435         .jdbcType(jdbcType)
436         .nestedQueryId(applyCurrentNamespace(nestedSelect, true))
437         .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
438         .resultSet(resultSet)
439         .typeHandler(typeHandlerInstance)
440         .flags(flags == null ? new ArrayList<>() : flags)
441         .composites(composites)
442         .notNullColumns(parseMultipleColumnNames(notNullColumn))
443         .columnPrefix(columnPrefix)
444         .foreignColumn(foreignColumn)
445         .lazy(lazy)
446         .build();
447   }
448 
449   /**
450    * Backward compatibility signature 'buildResultMapping'.
451    *
452    * @param resultType
453    *          the result type
454    * @param property
455    *          the property
456    * @param column
457    *          the column
458    * @param javaType
459    *          the java type
460    * @param jdbcType
461    *          the jdbc type
462    * @param nestedSelect
463    *          the nested select
464    * @param nestedResultMap
465    *          the nested result map
466    * @param notNullColumn
467    *          the not null column
468    * @param columnPrefix
469    *          the column prefix
470    * @param typeHandler
471    *          the type handler
472    * @param flags
473    *          the flags
474    * @return the result mapping
475    */
476   public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, Class<?> javaType,
477       JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,
478       Class<? extends TypeHandler<?>> typeHandler, List<ResultFlag> flags) {
479     return buildResultMapping(
480       resultType, property, column, javaType, jdbcType, nestedSelect,
481       nestedResultMap, notNullColumn, columnPrefix, typeHandler, flags, null, null, configuration.isLazyLoadingEnabled());
482   }
483 
484   /**
485    * Gets the language driver.
486    *
487    * @param langClass
488    *          the lang class
489    * @return the language driver
490    * @deprecated Use {@link Configuration#getLanguageDriver(Class)}
491    */
492   @Deprecated
493   public LanguageDriver getLanguageDriver(Class<? extends LanguageDriver> langClass) {
494     return configuration.getLanguageDriver(langClass);
495   }
496 
497   private Set<String> parseMultipleColumnNames(String columnName) {
498     Set<String> columns = new HashSet<>();
499     if (columnName != null) {
500       if (columnName.indexOf(',') > -1) {
501         StringTokenizer parser = new StringTokenizer(columnName, "{}, ", false);
502         while (parser.hasMoreTokens()) {
503           String column = parser.nextToken();
504           columns.add(column);
505         }
506       } else {
507         columns.add(columnName);
508       }
509     }
510     return columns;
511   }
512 
513   private List<ResultMapping> parseCompositeColumnName(String columnName) {
514     List<ResultMapping> composites = new ArrayList<>();
515     if (columnName != null && (columnName.indexOf('=') > -1 || columnName.indexOf(',') > -1)) {
516       StringTokenizer parser = new StringTokenizer(columnName, "{}=, ", false);
517       while (parser.hasMoreTokens()) {
518         String property = parser.nextToken();
519         String column = parser.nextToken();
520         ResultMapping complexResultMapping = new ResultMapping.Builder(
521             configuration, property, column, configuration.getTypeHandlerRegistry().getUnknownTypeHandler()).build();
522         composites.add(complexResultMapping);
523       }
524     }
525     return composites;
526   }
527 
528   private Class<?> resolveResultJavaType(Class<?> resultType, String property, Class<?> javaType) {
529     if (javaType == null && property != null) {
530       try {
531         MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
532         javaType = metaResultType.getSetterType(property);
533       } catch (Exception e) {
534         // ignore, following null check statement will deal with the situation
535       }
536     }
537     if (javaType == null) {
538       javaType = Object.class;
539     }
540     return javaType;
541   }
542 
543   private Class<?> resolveParameterJavaType(Class<?> resultType, String property, Class<?> javaType, JdbcType jdbcType) {
544     if (javaType == null) {
545       if (JdbcType.CURSOR.equals(jdbcType)) {
546         javaType = java.sql.ResultSet.class;
547       } else if (Map.class.isAssignableFrom(resultType)) {
548         javaType = Object.class;
549       } else {
550         MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
551         javaType = metaResultType.getGetterType(property);
552       }
553     }
554     if (javaType == null) {
555       javaType = Object.class;
556     }
557     return javaType;
558   }
559 
560 }