MapperBuilderAssistant.java
/*
* Copyright 2009-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.builder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.decorators.LruCache;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.mapping.CacheBuilder;
import org.apache.ibatis.mapping.Discriminator;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMap;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.mapping.ResultFlag;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.mapping.ResultSetType;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
/**
* @author Clinton Begin
*/
public class MapperBuilderAssistant extends BaseBuilder {
private String currentNamespace;
private final String resource;
private Cache currentCache;
private boolean unresolvedCacheRef; // issue #676
public MapperBuilderAssistant(Configuration configuration, String resource) {
super(configuration);
ErrorContext.instance().resource(resource);
this.resource = resource;
}
public String getCurrentNamespace() {
return currentNamespace;
}
public void setCurrentNamespace(String currentNamespace) {
if (currentNamespace == null) {
throw new BuilderException("The mapper element requires a namespace attribute to be specified.");
}
if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) {
throw new BuilderException(
"Wrong namespace. Expected '" + this.currentNamespace + "' but found '" + currentNamespace + "'.");
}
this.currentNamespace = currentNamespace;
}
public String applyCurrentNamespace(String base, boolean isReference) {
if (base == null) {
return null;
}
if (isReference) {
// is it qualified with any namespace yet?
if (base.contains(".")) {
return base;
}
} else {
// is it qualified with this namespace yet?
if (base.startsWith(currentNamespace + ".")) {
return base;
}
if (base.contains(".")) {
throw new BuilderException("Dots are not allowed in element names, please remove it from " + base);
}
}
return currentNamespace + "." + base;
}
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval,
Integer size, boolean readWrite, boolean blocking, Properties props) {
Cache cache = new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size)
.readWrite(readWrite).blocking(blocking).properties(props).build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
public ParameterMap addParameterMap(String id, Class<?> parameterClass, List<ParameterMapping> parameterMappings) {
id = applyCurrentNamespace(id, false);
ParameterMap parameterMap = new ParameterMap.Builder(configuration, id, parameterClass, parameterMappings).build();
configuration.addParameterMap(parameterMap);
return parameterMap;
}
public ParameterMapping buildParameterMapping(Class<?> parameterType, String property, Class<?> javaType,
JdbcType jdbcType, String resultMap, ParameterMode parameterMode, Class<? extends TypeHandler<?>> typeHandler,
Integer numericScale) {
resultMap = applyCurrentNamespace(resultMap, true);
// Class parameterType = parameterMapBuilder.type();
Class<?> javaTypeClass = resolveParameterJavaType(parameterType, property, javaType, jdbcType);
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
return new ParameterMapping.Builder(configuration, property, javaTypeClass).jdbcType(jdbcType)
.resultMapId(resultMap).mode(parameterMode).numericScale(numericScale).typeHandler(typeHandlerInstance).build();
}
public ResultMap addResultMap(String id, Class<?> type, String extend, Discriminator discriminator,
List<ResultMapping> resultMappings, Boolean autoMapping) {
id = applyCurrentNamespace(id, false);
extend = applyCurrentNamespace(extend, true);
if (extend != null) {
if (!configuration.hasResultMap(extend)) {
throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
}
ResultMap resultMap = configuration.getResultMap(extend);
List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
extendedResultMappings.removeAll(resultMappings);
// Remove parent constructor if this resultMap declares a constructor.
boolean declaresConstructor = false;
for (ResultMapping resultMapping : resultMappings) {
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}
if (declaresConstructor) {
extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
}
resultMappings.addAll(extendedResultMappings);
}
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator).build();
configuration.addResultMap(resultMap);
return resultMap;
}
public Discriminator buildDiscriminator(Class<?> resultType, String column, Class<?> javaType, JdbcType jdbcType,
Class<? extends TypeHandler<?>> typeHandler, Map<String, String> discriminatorMap) {
ResultMapping resultMapping = buildResultMapping(resultType, null, column, javaType, jdbcType, null, null, null,
null, typeHandler, new ArrayList<>(), null, null, false);
Map<String, String> namespaceDiscriminatorMap = new HashMap<>();
for (Map.Entry<String, String> e : discriminatorMap.entrySet()) {
String resultMap = e.getValue();
resultMap = applyCurrentNamespace(resultMap, true);
namespaceDiscriminatorMap.put(e.getKey(), resultMap);
}
return new Discriminator.Builder(configuration, resultMapping, namespaceDiscriminatorMap).build();
}
public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType,
SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType,
String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache,
boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId,
LanguageDriver lang, String resultSets, boolean dirtySelect) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType)
.keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId).lang(lang)
.resultOrdered(resultOrdered).resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType)
.flushCacheRequired(flushCache).useCache(useCache).cache(currentCache).dirtySelect(dirtySelect);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
/**
* Backward compatibility signature 'addMappedStatement'.
*
* @param id
* the id
* @param sqlSource
* the sql source
* @param statementType
* the statement type
* @param sqlCommandType
* the sql command type
* @param fetchSize
* the fetch size
* @param timeout
* the timeout
* @param parameterMap
* the parameter map
* @param parameterType
* the parameter type
* @param resultMap
* the result map
* @param resultType
* the result type
* @param resultSetType
* the result set type
* @param flushCache
* the flush cache
* @param useCache
* the use cache
* @param resultOrdered
* the result ordered
* @param keyGenerator
* the key generator
* @param keyProperty
* the key property
* @param keyColumn
* the key column
* @param databaseId
* the database id
* @param lang
* the lang
*
* @return the mapped statement
*/
public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType,
SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType,
String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache,
boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId,
LanguageDriver lang, String resultSets) {
return addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
parameterType, resultMap, resultType, resultSetType, flushCache, useCache, resultOrdered, keyGenerator,
keyProperty, keyColumn, databaseId, lang, null, false);
}
public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType,
SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType,
String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache,
boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId,
LanguageDriver lang) {
return addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
parameterType, resultMap, resultType, resultSetType, flushCache, useCache, resultOrdered, keyGenerator,
keyProperty, keyColumn, databaseId, lang, null);
}
private <T> T valueOrDefault(T value, T defaultValue) {
return value == null ? defaultValue : value;
}
private ParameterMap getStatementParameterMap(String parameterMapName, Class<?> parameterTypeClass,
String statementId) {
parameterMapName = applyCurrentNamespace(parameterMapName, true);
ParameterMap parameterMap = null;
if (parameterMapName != null) {
try {
parameterMap = configuration.getParameterMap(parameterMapName);
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("Could not find parameter map " + parameterMapName, e);
}
} else if (parameterTypeClass != null) {
List<ParameterMapping> parameterMappings = new ArrayList<>();
parameterMap = new ParameterMap.Builder(configuration, statementId + "-Inline", parameterTypeClass,
parameterMappings).build();
}
return parameterMap;
}
private List<ResultMap> getStatementResultMaps(String resultMap, Class<?> resultType, String statementId) {
resultMap = applyCurrentNamespace(resultMap, true);
List<ResultMap> resultMaps = new ArrayList<>();
if (resultMap != null) {
String[] resultMapNames = resultMap.split(",");
for (String resultMapName : resultMapNames) {
try {
resultMaps.add(configuration.getResultMap(resultMapName.trim()));
} catch (IllegalArgumentException e) {
throw new IncompleteElementException(
"Could not find result map '" + resultMapName + "' referenced from '" + statementId + "'", e);
}
}
} else if (resultType != null) {
ResultMap inlineResultMap = new ResultMap.Builder(configuration, statementId + "-Inline", resultType,
new ArrayList<>(), null).build();
resultMaps.add(inlineResultMap);
}
return resultMaps;
}
public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, Class<?> javaType,
JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,
Class<? extends TypeHandler<?>> typeHandler, List<ResultFlag> flags, String resultSet, String foreignColumn,
boolean lazy) {
Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
List<ResultMapping> composites;
if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
composites = Collections.emptyList();
} else {
composites = parseCompositeColumnName(column);
}
return new ResultMapping.Builder(configuration, property, column, javaTypeClass).jdbcType(jdbcType)
.nestedQueryId(applyCurrentNamespace(nestedSelect, true))
.nestedResultMapId(applyCurrentNamespace(nestedResultMap, true)).resultSet(resultSet)
.typeHandler(typeHandlerInstance).flags(flags == null ? new ArrayList<>() : flags).composites(composites)
.notNullColumns(parseMultipleColumnNames(notNullColumn)).columnPrefix(columnPrefix).foreignColumn(foreignColumn)
.lazy(lazy).build();
}
/**
* Backward compatibility signature 'buildResultMapping'.
*
* @param resultType
* the result type
* @param property
* the property
* @param column
* the column
* @param javaType
* the java type
* @param jdbcType
* the jdbc type
* @param nestedSelect
* the nested select
* @param nestedResultMap
* the nested result map
* @param notNullColumn
* the not null column
* @param columnPrefix
* the column prefix
* @param typeHandler
* the type handler
* @param flags
* the flags
*
* @return the result mapping
*/
public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, Class<?> javaType,
JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,
Class<? extends TypeHandler<?>> typeHandler, List<ResultFlag> flags) {
return buildResultMapping(resultType, property, column, javaType, jdbcType, nestedSelect, nestedResultMap,
notNullColumn, columnPrefix, typeHandler, flags, null, null, configuration.isLazyLoadingEnabled());
}
/**
* Gets the language driver.
*
* @param langClass
* the lang class
*
* @return the language driver
*
* @deprecated Use {@link Configuration#getLanguageDriver(Class)}
*/
@Deprecated
public LanguageDriver getLanguageDriver(Class<? extends LanguageDriver> langClass) {
return configuration.getLanguageDriver(langClass);
}
private Set<String> parseMultipleColumnNames(String columnName) {
Set<String> columns = new HashSet<>();
if (columnName != null) {
if (columnName.indexOf(',') > -1) {
StringTokenizer parser = new StringTokenizer(columnName, "{}, ", false);
while (parser.hasMoreTokens()) {
String column = parser.nextToken();
columns.add(column);
}
} else {
columns.add(columnName);
}
}
return columns;
}
private List<ResultMapping> parseCompositeColumnName(String columnName) {
List<ResultMapping> composites = new ArrayList<>();
if (columnName != null && (columnName.indexOf('=') > -1 || columnName.indexOf(',') > -1)) {
StringTokenizer parser = new StringTokenizer(columnName, "{}=, ", false);
while (parser.hasMoreTokens()) {
String property = parser.nextToken();
String column = parser.nextToken();
ResultMapping complexResultMapping = new ResultMapping.Builder(configuration, property, column,
configuration.getTypeHandlerRegistry().getUnknownTypeHandler()).build();
composites.add(complexResultMapping);
}
}
return composites;
}
private Class<?> resolveResultJavaType(Class<?> resultType, String property, Class<?> javaType) {
if (javaType == null && property != null) {
try {
MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
javaType = metaResultType.getSetterType(property);
} catch (Exception e) {
// ignore, following null check statement will deal with the situation
}
}
if (javaType == null) {
javaType = Object.class;
}
return javaType;
}
private Class<?> resolveParameterJavaType(Class<?> resultType, String property, Class<?> javaType,
JdbcType jdbcType) {
if (javaType == null) {
if (JdbcType.CURSOR.equals(jdbcType)) {
javaType = java.sql.ResultSet.class;
} else if (Map.class.isAssignableFrom(resultType)) {
javaType = Object.class;
} else {
MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
javaType = metaResultType.getGetterType(property);
}
}
if (javaType == null) {
javaType = Object.class;
}
return javaType;
}
}